/*
Copyright 2018-2024 New Vector Ltd.
Copyright 2017 Vector Creations Ltd
Copyright 2014 OpenMarket Ltd

SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
 */

@import MobileCoreServices;

#import "RoomViewController.h"

#import "RoomDataSource.h"
#import "RoomBubbleCellData.h"

#import "RoomInputToolbarView.h"
#import "DisabledRoomInputToolbarView.h"

#import "RoomActivitiesView.h"

#import "AttachmentsViewController.h"

#import "EventDetailsView.h"

#import "RoomAvatarTitleView.h"
#import "ExpandedRoomTitleView.h"
#import "SimpleRoomTitleView.h"
#import "PreviewRoomTitleView.h"

#import "RoomMemberDetailsViewController.h"
#import "ContactDetailsViewController.h"

#import "SegmentedViewController.h"
#import "RoomSettingsViewController.h"

#import "RoomFilesViewController.h"

#import "RoomSearchViewController.h"

#import "UsersDevicesViewController.h"

#import "ReadReceiptsViewController.h"

#import "JitsiViewController.h"

#import "RoomEmptyBubbleCell.h"
#import "RoomMembershipExpandedBubbleCell.h"
#import "MXKRoomBubbleTableViewCell+Riot.h"

#import "AvatarGenerator.h"
#import "Tools.h"
#import "WidgetManager.h"
#import "ShareManager.h"

#import "GBDeviceInfo_iOS.h"

#import "RoomEncryptedDataBubbleCell.h"
#import "EncryptionInfoView.h"

#import "MXRoom+Riot.h"

#import "IntegrationManagerViewController.h"
#import "WidgetPickerViewController.h"
#import "StickerPickerViewController.h"

#import "EventFormatter.h"

#import "SettingsViewController.h"
#import "SecurityViewController.h"

#import "TypingUserInfo.h"

#import "MXSDKOptions.h"

#import "RoomTimelineCellProvider.h"

#import "GeneratedInterface-Swift.h"

NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNotification";
NSNotificationName const RoomGroupCallTileTappedNotification = @"RoomGroupCallTileTappedNotification";
const NSTimeInterval kResizeComposerAnimationDuration = .05;
static const int kThreadListBarButtonItemTag = 99;
static UIEdgeInsets kThreadListBarButtonItemContentInsetsNoDot;
static UIEdgeInsets kThreadListBarButtonItemContentInsetsDot;
static CGSize kThreadListBarButtonItemImageSize;

@interface RoomViewController () <UISearchBarDelegate, UIGestureRecognizerDelegate, UIScrollViewAccessibilityDelegate, RoomTitleViewTapGestureDelegate, MXKRoomMemberDetailsViewControllerDelegate, ContactsTableViewControllerDelegate, MXServerNoticesDelegate, RoomContextualMenuViewControllerDelegate,
    ReactionsMenuViewModelCoordinatorDelegate, EditHistoryCoordinatorBridgePresenterDelegate, MXKDocumentPickerPresenterDelegate, EmojiPickerCoordinatorBridgePresenterDelegate,
    ReactionHistoryCoordinatorBridgePresenterDelegate, CameraPresenterDelegate, MediaPickerCoordinatorBridgePresenterDelegate,
    RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate, CompletionSuggestionCoordinatorBridgeDelegate, ThreadsCoordinatorBridgePresenterDelegate, ThreadsBetaCoordinatorBridgePresenterDelegate, MXThreadingServiceDelegate, RoomParticipantsInviteCoordinatorBridgePresenterDelegate, RoomInputToolbarViewDelegate, ComposerCreateActionListBridgePresenterDelegate>
{
    
    // The preview header
    __weak PreviewRoomTitleView *previewHeader;
    
    // The user taps on a user id contained in a message
    MXKContact *selectedContact;
    
    // List of members who are typing in the room.
    NSArray *currentTypingUsers;
    
    // Typing notifications listener.
    __weak id typingNotifListener;
    
    // The position of the first touch down event stored in case of scrolling when the expanded header is visible.
    CGPoint startScrollingPoint;
    
    // Missed discussions badge
    NSUInteger missedDiscussionsCount;
    NSUInteger missedHighlightCount;
    UILabel *missedDiscussionsBadgeLabel;
    UIView *missedDiscussionsDotView;
    
    // Potential encryption details view.
    __weak EncryptionInfoView *encryptionInfoView;
    
    // The list of unknown devices that prevent outgoing messages from being sent
    MXUsersDevicesMap<MXDeviceInfo*> *unknownDevices;
    
    // Observe kAppDelegateDidTapStatusBarNotification to handle tap on clock status bar.
    __weak id kAppDelegateDidTapStatusBarNotificationObserver;
    
    // Observe kAppDelegateNetworkStatusDidChangeNotification to handle network status change.
    __weak id kAppDelegateNetworkStatusDidChangeNotificationObserver;

    // Observers to manage MXSession state (and sync errors)
    __weak id kMXSessionStateDidChangeObserver;

    // Observers to manage ongoing conference call banner
    __weak id kMXCallStateDidChangeObserver;
    __weak id kMXCallManagerConferenceStartedObserver;
    __weak id kMXCallManagerConferenceFinishedObserver;

    // Observers to manage widgets
    __weak id kMXKWidgetManagerDidUpdateWidgetObserver;
    
    // Observer kMXRoomSummaryDidChangeNotification to keep updated the missed discussion count
    __weak id mxRoomSummaryDidChangeObserver;

    // Observer for removing the re-request explanation/waiting dialog
    __weak id mxEventDidDecryptNotificationObserver;
    
    // The table view cell in which the read marker is displayed (nil by default).
    MXKRoomBubbleTableViewCell *readMarkerTableViewCell;
    
    // Tell whether the view controller is appeared or not.
    BOOL isAppeared;
    
    // A flag indicating whether a room has been left
    BOOL isRoomLeft;
    
    // The last known frame of the view used to detect whether size-related layout change is needed
    CGRect lastViewBounds;
    
    // Tell whether the room has a Jitsi call or not.
    BOOL hasJitsiCall;
    
    // The right bar button items back up.
    NSArray<UIBarButtonItem *> *rightBarButtonItems;

    // Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change.
    __weak id kThemeServiceDidChangeThemeNotificationObserver;
    
    // Observe URL preview updates to refresh cells.
    __weak id URLPreviewDidUpdateNotificationObserver;
    
    // Listener for `m.room.tombstone` event type
    __weak id tombstoneEventNotificationsListener;

    // Homeserver notices
    MXServerNotices *serverNotices;
    
    // Formatted body parser for events
    FormattedBodyParser *formattedBodyParser;
    
    // Time to display notification content in the timeline
    MXTaskProfile *notificationTaskProfile;
    
    // Observe kMXEventTypeStringRoomMember events
    __weak id roomMemberEventListener;
}

@property (nonatomic, strong) RemoveJitsiWidgetView *removeJitsiWidgetView;


@property (nonatomic, strong) RoomContextualMenuViewController *roomContextualMenuViewController;
@property (nonatomic, strong) RoomContextualMenuPresenter *roomContextualMenuPresenter;
@property (nonatomic, strong) MXKErrorAlertPresentation *errorPresenter;
@property (nonatomic, strong) NSAttributedString *textMessageBeforeEditing;
@property (nonatomic, strong) NSString *htmlTextBeforeEditing;
@property (nonatomic, strong) EditHistoryCoordinatorBridgePresenter *editHistoryPresenter;
@property (nonatomic, strong) MXKDocumentPickerPresenter *documentPickerPresenter;
@property (nonatomic, strong) EmojiPickerCoordinatorBridgePresenter *emojiPickerCoordinatorBridgePresenter;
@property (nonatomic, strong) ReactionHistoryCoordinatorBridgePresenter *reactionHistoryCoordinatorBridgePresenter;
@property (nonatomic, strong) CameraPresenter *cameraPresenter;
@property (nonatomic, strong) MediaPickerCoordinatorBridgePresenter *mediaPickerPresenter;
@property (nonatomic, strong) RoomMessageURLParser *roomMessageURLParser;
@property (nonatomic, strong) RoomCreationModalCoordinatorBridgePresenter *roomCreationModalCoordinatorBridgePresenter;
@property (nonatomic, strong) RoomInfoCoordinatorBridgePresenter *roomInfoCoordinatorBridgePresenter;
@property (nonatomic, strong) CustomSizedPresentationController *customSizedPresentationController;
@property (nonatomic, strong) RoomParticipantsInviteCoordinatorBridgePresenter *participantsInvitePresenter;
@property (nonatomic, strong) ThreadsCoordinatorBridgePresenter *threadsBridgePresenter;
@property (nonatomic, strong) ThreadsBetaCoordinatorBridgePresenter *threadsBetaBridgePresenter;
@property (nonatomic, strong) SlidingModalPresenter *threadsNoticeModalPresenter;
@property (nonatomic, strong) ComposerCreateActionListBridgePresenter *composerCreateActionListBridgePresenter;
@property (nonatomic, getter=isActivitiesViewExpanded) BOOL activitiesViewExpanded;
@property (nonatomic, getter=isScrollToBottomHidden) BOOL scrollToBottomHidden;
@property (nonatomic, getter=isMissedDiscussionsBadgeHidden) BOOL missedDiscussionsBadgeHidden;

@property (nonatomic, strong) VoiceMessageController *voiceMessageController;
@property (nonatomic, strong) SpaceDetailPresenter *spaceDetailPresenter;

@property (nonatomic, strong) ShareManager *shareManager;
@property (nonatomic, strong) EventMenuBuilder *eventMenuBuilder;

@property (nonatomic, strong) CompletionSuggestionCoordinatorBridge *completionSuggestionCoordinator;
@property (nonatomic, weak) IBOutlet UIView *completionSuggestionContainerView;

@property (nonatomic, readwrite) RoomDisplayConfiguration *displayConfiguration;

// The direct chat target user. The room timeline is presented without an actual room until the direct chat is created
@property (nonatomic, nullable, strong) MXUser *directChatTargetUser;

// When layout of the screen changes (e.g. height), we no longer know whether
// to autoscroll to the bottom again or not. Instead we need to capture the
// scroll state just before the layout change, and restore it after the layout.
@property (nonatomic) BOOL wasScrollAtBottomBeforeLayout;

// Check if we should wait for other participants
@property (nonatomic, readonly) BOOL shouldWaitForOtherParticipants;

@end

@implementation RoomViewController
@synthesize roomPreviewData;

#pragma mark - Class methods

+ (void)initialize
{
    kThreadListBarButtonItemContentInsetsNoDot = UIEdgeInsetsMake(0, 8, 0, 8);
    kThreadListBarButtonItemContentInsetsDot = UIEdgeInsetsMake(0, 8, 6, 8);
    kThreadListBarButtonItemImageSize = CGSizeMake(21, 21);
}

+ (UINib *)nib
{
    return [UINib nibWithNibName:NSStringFromClass(self.class)
                          bundle:[NSBundle bundleForClass:self.class]];
}

+ (instancetype)roomViewController
{
    RoomViewController *controller = [[[self class] alloc] initWithNibName:NSStringFromClass(self.class)
                                                                    bundle:[NSBundle bundleForClass:self.class]];
    controller.displayConfiguration = [RoomDisplayConfiguration default];
    return controller;
}

+ (instancetype)instantiateWithConfiguration:(RoomDisplayConfiguration *)configuration
{
    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]];
    NSString *storyboardId = [NSString stringWithFormat:@"%@StoryboardId", self.className];
    RoomViewController *controller = [storyboard instantiateViewControllerWithIdentifier:storyboardId];
    controller.displayConfiguration = configuration;
    return controller;
}

+ (NSString *)className
{
    NSString *result = NSStringFromClass(self.class);
    if ([result containsString:@"."])
    {
        result = [result componentsSeparatedByString:@"."].lastObject;
    }
    return result;
}

#pragma mark -

- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self)
    {
        // Disable auto join
        self.autoJoinInvitedRoom = NO;
        
        // Disable auto scroll to bottom on keyboard presentation
        self.scrollHistoryToTheBottomOnKeyboardPresentation = NO;
    }
    
    return self;
}

- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if (self)
    {
        // Disable auto join
        self.autoJoinInvitedRoom = NO;
        
        // Disable auto scroll to bottom on keyboard presentation
        self.scrollHistoryToTheBottomOnKeyboardPresentation = NO;
    }
    
    return self;
}

#pragma mark -

- (void)finalizeInit
{
    [super finalizeInit];

    [self registerPillAttachmentViewProviderIfNeeded];
    self.resizeComposerAnimationDuration = kResizeComposerAnimationDuration;
    
    // Setup `MXKViewControllerHandling` properties
    self.enableBarTintColorStatusChange = NO;
    self.rageShakeManager = [RageShakeManager sharedManager];
    formattedBodyParser = [FormattedBodyParser new];
    self.eventMenuBuilder = [EventMenuBuilder new];
    
    _showMissedDiscussionsBadge = YES;
    _scrollToBottomHidden = YES;
    _isWaitingForOtherParticipants = NO;
    
    // Listen to the event sent state changes
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(eventDidChangeSentState:) name:kMXEventDidChangeSentStateNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(eventDidChangeIdentifier:) name:kMXEventDidChangeIdentifierNotification object:nil];
    
    // Show / hide actions button in document preview according BuildSettings
    self.allowActionsInDocumentPreview = BuildSettings.messageDetailsAllowShare;
    
    _voiceMessageController = [[VoiceMessageController alloc] initWithThemeService:ThemeService.shared mediaServiceProvider:VoiceMessageMediaServiceProvider.sharedProvider];
    self.voiceMessageController.delegate = self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    // Register first customized cell view classes used to render bubbles
    [[RoomTimelineConfiguration shared].currentStyle.cellProvider registerCellsForTableView:self.bubblesTableView];
    
    [self vc_removeBackTitle];
    
    // Display leftBarButtonItems or leftBarButtonItem to the right of the Back button
    self.navigationItem.leftItemsSupplementBackButton = YES;
    
    [self setupRemoveJitsiWidgetRemoveView];
    
    dispatch_async(dispatch_get_main_queue(), ^{
        // Replace the default input toolbar view.
        // Note: this operation will force the layout of subviews. That is why cell view classes must be registered before.
        [self updateRoomInputToolbarViewClassIfNeeded];
    });
    
    // set extra area
    [self setRoomActivitiesViewClass:RoomActivitiesView.class];
    
    // Custom the attachmnet viewer
    [self setAttachmentsViewerClass:AttachmentsViewController.class];
    
    // Custom the event details view
    [self setEventDetailsViewClass:EventDetailsView.class];
    
    // Prepare missed dicussion badge (if any)
    self.showMissedDiscussionsBadge = _showMissedDiscussionsBadge;

    // Refresh the waiting for other participants state
    [self refreshWaitForOtherParticipantsState];

    // Set up the room title view according to the data source (if any)
    [self refreshRoomTitle];
    
    // Refresh tool bar if the room data source is set.
    if (self.roomDataSource)
    {
        [self refreshRoomInputToolbar];
    }
    
    self.roomContextualMenuPresenter = [RoomContextualMenuPresenter new];
    self.errorPresenter = [MXKErrorAlertPresentation new];
    self.roomMessageURLParser = [RoomMessageURLParser new];
    
    self.jumpToLastUnreadLabel.text = [VectorL10n roomJumpToFirstUnread];
    
    MXWeakify(self);
    
    // Observe user interface theme change.
    kThemeServiceDidChangeThemeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kThemeServiceDidChangeThemeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
        
        MXStrongifyAndReturnIfNil(self);
        
        [self userInterfaceThemeDidChange];
        
    }];
    
    [self userInterfaceThemeDidChange];
    
    // Observe URL preview updates.
    [self registerURLPreviewNotifications];
    
    [self setupActions];
    
    [self setupCompletionSuggestionViewIfNeeded];
    
    [self.topBannersStackView vc_removeAllSubviews];
}

- (void)userInterfaceThemeDidChange
{
    // Consider the main navigation controller if the current view controller is embedded inside a split view controller.
    UINavigationController *mainNavigationController = self.navigationController;
    if (self.splitViewController.isCollapsed && self.splitViewController.viewControllers.count)
    {
        mainNavigationController = self.splitViewController.viewControllers.firstObject;
    }
    
    [ThemeService.shared.theme applyStyleOnNavigationBar:self.navigationController.navigationBar];
    if (mainNavigationController)
    {
        [ThemeService.shared.theme applyStyleOnNavigationBar:mainNavigationController.navigationBar];
    }
    
    // Keep navigation bar transparent in some cases
    if (!self.previewHeaderContainer.hidden)
    {
        self.navigationController.navigationBar.translucent = YES;
        mainNavigationController.navigationBar.translucent = YES;
    }
    
    [self.inputToolbarView customizeViewRendering];
    
    self.activityIndicator.backgroundColor = ThemeService.shared.theme.overlayBackgroundColor;
    
    [self.removeJitsiWidgetView updateWithTheme:ThemeService.shared.theme];
    
    // Prepare jump to last unread banner
    self.jumpToLastUnreadImageView.tintColor = ThemeService.shared.theme.tintColor;
    self.jumpToLastUnreadLabel.textColor = ThemeService.shared.theme.textPrimaryColor;
    
    self.previewHeaderContainer.backgroundColor = ThemeService.shared.theme.headerBackgroundColor;
    
    // Check the table view style to select its bg color.
    self.bubblesTableView.backgroundColor = ((self.bubblesTableView.style == UITableViewStylePlain) ? ThemeService.shared.theme.backgroundColor : ThemeService.shared.theme.headerBackgroundColor);
    self.bubblesTableView.separatorColor = ThemeService.shared.theme.lineBreakColor;
    self.view.backgroundColor = self.bubblesTableView.backgroundColor;
    
    if (self.bubblesTableView.dataSource)
    {
        [self.bubblesTableView reloadData];
    }
    
    [self.scrollToBottomButton vc_addShadowWithColor:ThemeService.shared.theme.shadowColor
                                              offset:CGSizeMake(0, 4)
                                              radius:6
                                             opacity:0.2];

    self.inputBackgroundView.backgroundColor = [ThemeService.shared.theme.backgroundColor colorWithAlphaComponent:0.98];
    
    if (ThemeService.shared.isCurrentThemeDark)
    {
        [self.scrollToBottomButton setImage:AssetImages.scrolldownDark.image forState:UIControlStateNormal];

        self.jumpToLastUnreadBanner.backgroundColor = ThemeService.shared.theme.colors.navigation;
        [self.jumpToLastUnreadBanner vc_removeShadow];
        self.resetReadMarkerButton.tintColor = ThemeService.shared.theme.colors.quarterlyContent;
        if (self.maximisedToolbarDimmingView) {
            self.maximisedToolbarDimmingView.backgroundColor = [UIColor.blackColor colorWithAlphaComponent:0.29];
        }
    }
    else
    {
        [self.scrollToBottomButton setImage:AssetImages.scrolldown.image forState:UIControlStateNormal];
        
        self.jumpToLastUnreadBanner.backgroundColor = ThemeService.shared.theme.colors.background;
        [self.jumpToLastUnreadBanner vc_addShadowWithColor:ThemeService.shared.theme.shadowColor
                                                    offset:CGSizeMake(0, 4)
                                                    radius:8
                                                   opacity:0.1];
        self.resetReadMarkerButton.tintColor = ThemeService.shared.theme.colors.tertiaryContent;
        if (self.maximisedToolbarDimmingView) {
            self.maximisedToolbarDimmingView.backgroundColor = [UIColor.blackColor colorWithAlphaComponent:0.12];
        }
    }
    
    self.scrollToBottomBadgeLabel.badgeColor = ThemeService.shared.theme.tintColor;
    
    [self updateThreadListBarButtonBadgeWith:self.mainSession.threadingService];
    
    [self.liveLocationSharingBannerView updateWithTheme:ThemeService.shared.theme];
    
    [self setNeedsStatusBarAppearanceUpdate];
}

- (UIStatusBarStyle)preferredStatusBarStyle
{
    return ThemeService.shared.theme.statusBarStyle;
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    
    // Refresh the room title view
    [self refreshRoomTitle];
    
    //  refresh remove Jitsi widget view
    [self refreshRemoveJitsiWidgetView];
    
    // Refresh tool bar if the room data source is set.
    if (self.roomDataSource)
    {
        [self refreshRoomInputToolbar];
    }
    
    // Reset typing notification in order to remove the allocated space
    if ([self.roomDataSource isKindOfClass:RoomDataSource.class])
    {
        [((RoomDataSource*)self.roomDataSource) resetTypingNotification];
    }

    [self listenTypingNotifications];
    [self listenCallNotifications];
    [self listenWidgetNotifications];
    [self listenTombstoneEventNotifications];
    [self listenMXSessionStateChangeNotifications];
    
    MXWeakify(self);
    
    // Observe kAppDelegateDidTapStatusBarNotification.
    kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
        
        MXStrongifyAndReturnIfNil(self);
        
        [self setBubbleTableViewContentOffset:CGPointMake(-self.bubblesTableView.adjustedContentInset.left, -self.bubblesTableView.adjustedContentInset.top) animated:YES];
    }];
    
    if ([self.roomDataSource.roomId isEqualToString:[LegacyAppDelegate theDelegate].lastNavigatedRoomIdFromPush])
    {
        [self startActivityIndicator];
        [self.roomDataSource reload];
        [LegacyAppDelegate theDelegate].lastNavigatedRoomIdFromPush = nil;
        
        notificationTaskProfile = [MXSDKOptions.sharedInstance.profiler startMeasuringTaskWithName:MXTaskProfileNameNotificationsOpenEvent];
    }
    
    [self updateTopBanners];
    
    self.bubblesTableView.clipsToBounds = NO;
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    
    // hide action
    if (currentAlert)
    {
        [currentAlert dismissViewControllerAnimated:NO completion:nil];
        currentAlert = nil;
    }
    
    [self removeTypingNotificationsListener];
    
    if (self.customizedRoomDataSource)
    {
        // Cancel potential selected event (to leave edition mode)
        if (self.customizedRoomDataSource.selectedEventId)
        {
            [self cancelEventSelection];
        }
    }
    [self cancelEventHighlight];
    
    // Hide preview header to restore navigation bar settings
    [self showPreviewHeader:NO];
    
    if (kAppDelegateDidTapStatusBarNotificationObserver)
    {
        [[NSNotificationCenter defaultCenter] removeObserver:kAppDelegateDidTapStatusBarNotificationObserver];
        kAppDelegateDidTapStatusBarNotificationObserver = nil;
    }
    
    [self removeCallNotificationsListeners];
    [self removeWidgetNotificationsListeners];
    [self removeTombstoneEventNotificationsListener];
    [self removeMXSessionStateChangeNotificationsListener];
    
    // Re-enable the read marker display, and disable its update.
    self.roomDataSource.showReadMarker = YES;
    self.updateRoomReadMarker = NO;
    isAppeared = NO;
    
    [VoiceMessageMediaServiceProvider.sharedProvider pauseAllServices];
    [VoiceBroadcastRecorderProvider.shared pauseRecording];
    [VoiceBroadcastPlaybackProvider.shared pausePlaying];
    
    // Stop the loading indicator even if the session is still in progress
    [self stopLoadingUserIndicator];
    
    [self setMaximisedToolbarIsHiddenIfNeeded: YES];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    // Screen tracking
    MXRoomSummary *summary = [self.mainSession roomWithRoomId:self.roomDataSource.roomId].summary;
    if (!summary || !summary.isJoined)
    {
        [AnalyticsScreenTracker trackScreen: AnalyticsScreenRoomPreview];
    }
    else
    {
        [AnalyticsScreenTracker trackScreen: AnalyticsScreenRoom];
    }

    isAppeared = YES;
    [self checkReadMarkerVisibility];
    
    if (self.roomDataSource)
    {
        // Set visible room id
        [AppDelegate theDelegate].visibleRoomId = self.roomDataSource.roomId;
    }
    
    MXWeakify(self);
    
    // Observe network reachability
    kAppDelegateNetworkStatusDidChangeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateNetworkStatusDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
        
        MXStrongifyAndReturnIfNil(self);
        
        [self refreshActivitiesViewDisplay];
        
    }];
    [self refreshActivitiesViewDisplay];
    [self refreshJumpToLastUnreadBannerDisplay];
    
    // Observe missed notifications
    mxRoomSummaryDidChangeObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXRoomSummaryDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
        
        MXStrongifyAndReturnIfNil(self);
        
        MXRoomSummary *roomSummary = notif.object;
        
        if ([roomSummary.roomId isEqualToString:self.roomDataSource.roomId])
        {
            [self refreshMissedDiscussionsCount:NO];
        }
    }];
    [self refreshMissedDiscussionsCount:YES];
    self.keyboardHeight = MAX(self.keyboardHeight, 0);
    
    if (hasJitsiCall &&
        !self.isRoomHavingAJitsiCall)
    {
        //  the room had a Jitsi call before, but not now
        hasJitsiCall = NO;
        [self reloadBubblesTable:YES];
    }
    
    self.showSettingsInitially = NO;

    if (!RiotSettings.shared.threadsNoticeDisplayed && RiotSettings.shared.enableThreads)
    {
        [self showThreadsNotice];
    }

    if (self.saveProgressTextInput && self.roomDataSource)
    {
        // Retrieve the potential message partially typed during last room display.
        // Note: We have to wait for viewDidAppear before updating growingTextView (viewWillAppear is too early)
        [self.inputToolbarView setPartialContent:self.roomDataSource.partialAttributedTextMessage];
    }
    
    [self setMaximisedToolbarIsHiddenIfNeeded: NO];
}

- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];
    
    // Hide contextual menu if needed
    [self hideContextualMenuAnimated:NO];
    
    // Reset visible room id
    [AppDelegate theDelegate].visibleRoomId = nil;
    
    if (kAppDelegateNetworkStatusDidChangeNotificationObserver)
    {
        [[NSNotificationCenter defaultCenter] removeObserver:kAppDelegateNetworkStatusDidChangeNotificationObserver];
        kAppDelegateNetworkStatusDidChangeNotificationObserver = nil;
    }
    
    if (mxRoomSummaryDidChangeObserver)
    {
        [[NSNotificationCenter defaultCenter] removeObserver:mxRoomSummaryDidChangeObserver];
        mxRoomSummaryDidChangeObserver = nil;
    }
    
    if (mxEventDidDecryptNotificationObserver)
    {
        [[NSNotificationCenter defaultCenter] removeObserver:mxEventDidDecryptNotificationObserver];
        mxEventDidDecryptNotificationObserver = nil;
    }
        
    if (self.isRoomHavingAJitsiCall)
    {
        hasJitsiCall = YES;
        [self reloadBubblesTable:YES];
    }
}

- (void)viewWillLayoutSubviews {
    [super viewWillLayoutSubviews];
    self.wasScrollAtBottomBeforeLayout = self.isBubblesTableScrollViewAtTheBottom;
}

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    BOOL didViewChangeBounds = !CGRectEqualToRect(lastViewBounds, self.view.bounds);
    lastViewBounds = self.view.bounds;
    
    UIEdgeInsets contentInset = self.bubblesTableView.contentInset;
    contentInset.bottom = self.view.safeAreaInsets.bottom;
    self.bubblesTableView.contentInset = contentInset;
    
    // Check here whether a subview has been added or removed
    if (encryptionInfoView)
    {
        if (!encryptionInfoView.superview)
        {
            // Reset
            encryptionInfoView = nil;
            
            // Reload the full table to take into account a potential change on a device status.
            [self.bubblesTableView reloadData];
        }
    }
    
    if (eventDetailsView)
    {
        if (!eventDetailsView.superview)
        {
            // Reset
            eventDetailsView = nil;
        }
    }
    
    // Check whether the preview header is visible
    if (previewHeader)
    {
        if (previewHeader.mainHeaderContainer.isHidden)
        {
            // Check here the main background height to display a correct navigation bar background.
            CGRect frame = self.navigationController.navigationBar.frame;
            
            CGFloat mainHeaderBackgroundHeight = frame.size.height + (frame.origin.y > 0 ? frame.origin.y : 0);
            
            if (previewHeader.mainHeaderBackgroundHeightConstraint.constant != mainHeaderBackgroundHeight)
            {
                previewHeader.mainHeaderBackgroundHeightConstraint.constant = mainHeaderBackgroundHeight;
                
                // Force the layout of previewHeader to update the position of 'bottomBorderView' which
                // is used to define the actual height of the preview container.
                [previewHeader layoutIfNeeded];
            }
        }
        
        self.edgesForExtendedLayout = UIRectEdgeAll;
        
        // Adjust the top constraint of the bubbles table
        CGRect frame = previewHeader.bottomBorderView.frame;
        self.previewHeaderContainerHeightConstraint.constant = frame.origin.y + frame.size.height;
        
        self.bubblesTableViewTopConstraint.constant = self.previewHeaderContainerHeightConstraint.constant - self.bubblesTableView.adjustedContentInset.top;
    }
    else
    {
        // In non expanded header mode, the navigation bar is opaque
        // The table view must not display behind it
        self.edgesForExtendedLayout = UIRectEdgeLeft | UIRectEdgeBottom | UIRectEdgeRight;
    }
    
    // re-scroll to the bottom, if at bottom before the most recent layout
    if (self.wasScrollAtBottomBeforeLayout && didViewChangeBounds)
    {
        self.wasScrollAtBottomBeforeLayout = NO;
        [self scrollBubblesTableViewToBottomAnimated:NO];
    }
    
    [self refreshMissedDiscussionsCount:YES];
}

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator
{
    if ([self.titleView isKindOfClass:RoomTitleView.class])
    {
        RoomTitleView *roomTitleView = (RoomTitleView*)self.titleView;
        if (UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation))
        {
            [roomTitleView updateLayoutForOrientation:UIInterfaceOrientationPortrait];
        }
        else
        {
            [roomTitleView updateLayoutForOrientation:UIInterfaceOrientationLandscapeLeft];
        }
    }

    // Hide the expanded header or the preview in case of iPad and iPhone 6 plus.
    // On these devices, the display mode of the splitviewcontroller may change during screen rotation.
    // It may correspond to an overlay mode in portrait and a side-by-side mode in landscape.
    // This display mode change involves a change at the navigation bar level.
    // If we don't hide the header, the navigation bar is in a wrong state after rotation. FIXME: Find a way to keep visible the header on rotation.
    if ([GBDeviceInfo deviceInfo].family == GBDeviceFamilyiPad || [GBDeviceInfo deviceInfo].displayInfo.display >= GBDeviceDisplay5p5Inch)
    {
        // Hide the preview header (if any) before rotating (It will be restored by `refreshRoomTitle` call if this is still a room preview).
        [self showPreviewHeader:NO];
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((coordinator.transitionDuration + 0.5) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            
            // Let [self refreshRoomTitle] refresh this title view correctly
            [self refreshRoomTitle];
            
        });
    }
    else if (previewHeader)
    {
        // Refresh here the preview header according to the coming screen orientation.
        
        // Retrieve the affine transform indicating the amount of rotation being applied to the interface.
        // This transform is the identity transform when no rotation is applied.
        // Otherwise, it is a transform that applies a 90 degree, -90 degree, or 180 degree rotation.
        CGAffineTransform transform = coordinator.targetTransform;
        
        // Consider here only the transform that applies a +/- 90 degree.
        if (transform.b * transform.c == -1)
        {
            UIInterfaceOrientation currentScreenOrientation = [[UIApplication sharedApplication] statusBarOrientation];
            BOOL isLandscapeOriented = YES;
            
            switch (currentScreenOrientation)
            {
                case UIInterfaceOrientationLandscapeRight:
                case UIInterfaceOrientationLandscapeLeft:
                {
                    // We leave here landscape orientation
                    isLandscapeOriented = NO;
                    break;
                }
                default:
                    break;
            }
            
            [self refreshPreviewHeader:isLandscapeOriented];
        }
    }
    else
    {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((coordinator.transitionDuration + 0.5) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            
            // Refresh the room title at the end of the transition to take into account the potential changes during the transition.
            // For example the display of a preview header is ignored during transition.
            [self refreshRoomTitle];
            
        });
    }
    
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
}

#pragma mark - Accessibility

// Handle scrolling when VoiceOver is on because it does not work well if we let the system do:
// VoiceOver loses the focus on the tableview
- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction
{
    BOOL canScroll = YES;
    
    // Scroll by one page
    CGFloat tableViewHeight = self.bubblesTableView.frame.size.height;
    
    CGPoint offset = self.bubblesTableView.contentOffset;
    switch (direction)
    {
        case UIAccessibilityScrollDirectionUp:
            offset.y -= tableViewHeight;
            break;
            
        case UIAccessibilityScrollDirectionDown:
            offset.y += tableViewHeight;
            break;
            
        default:
            break;
    }
    
    if (offset.y < 0 && ![self.roomDataSource.timeline canPaginate:MXTimelineDirectionBackwards])
    {
        // Can't paginate more. Let's stick on the first item
        UIView *focusedView = [self firstCellWithAccessibilityDataInCells:self.bubblesTableView.visibleCells.objectEnumerator];
        UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, focusedView);
        canScroll = NO;
    }
    else if (offset.y > self.bubblesTableView.contentSize.height - tableViewHeight
             && ![self.roomDataSource.timeline canPaginate:MXTimelineDirectionForwards])
    {
        // Can't paginate more. Let's stick on the last item with accessibility
        UIView *focusedView = [self firstCellWithAccessibilityDataInCells:self.bubblesTableView.visibleCells.reverseObjectEnumerator];
        UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, focusedView);
        canScroll = NO;
    }
    else
    {
        // Disable VoiceOver while scrolling
        self.bubblesTableView.accessibilityElementsHidden = YES;
        
        [self setBubbleTableViewContentOffset:offset animated:NO];
        
        NSEnumerator<UITableViewCell*> *cells;
        if (direction == UIAccessibilityScrollDirectionUp)
        {
            cells = self.bubblesTableView.visibleCells.objectEnumerator;
        }
        else
        {
            cells = self.bubblesTableView.visibleCells.reverseObjectEnumerator;
        }
        UIView *cell = [self firstCellWithAccessibilityDataInCells:cells];
        
        self.bubblesTableView.accessibilityElementsHidden = NO;
        
        // Force VoiceOver to focus on a visible item
        UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, cell);
    }
    
    // If we cannot scroll, let VoiceOver indicates the border
    return canScroll;
}

- (UIView*)firstCellWithAccessibilityDataInCells:(NSEnumerator<UITableViewCell*>*)cells
{
    UIView *view;
    
    for (UITableViewCell *cell in cells)
    {
        if (![cell isKindOfClass:[RoomEmptyBubbleCell class]])
        {
            view = cell;
            break;
        }
    }
    
    return view;
}


#pragma mark - Override MXKRoomViewController

- (void)addMatrixSession:(MXSession *)mxSession
{
    [super addMatrixSession:mxSession];
    
    [mxSession.threadingService addDelegate:self];
    [self updateThreadListBarButtonBadgeWith:mxSession.threadingService];
}

- (void)removeMatrixSession:(MXSession *)mxSession
{
    [mxSession.threadingService removeDelegate:self];
    
    [super removeMatrixSession:mxSession];
}

- (void)onMatrixSessionChange
{
    [super onMatrixSessionChange];
    
    // Re-enable the read marker display, and disable its update.
    self.roomDataSource.showReadMarker = YES;
    self.updateRoomReadMarker = NO;
}

#pragma mark - Loading indicators

- (BOOL)providesCustomActivityIndicator {
    return YES;
}

// Override of a legacy method to determine whether to use a newer implementation instead.
// Will be removed in the future https://github.com/vector-im/element-ios/issues/5608
- (void)startActivityIndicator {
    [self.delegate roomViewControllerDidStartLoading:self];
}

// Override of a legacy method to determine whether to use a newer implementation instead.
// Will be removed in the future https://github.com/vector-im/element-ios/issues/5608
- (void)stopActivityIndicator
{
    if (notificationTaskProfile)
    {
        // Consider here we have displayed the message corresponding to the notification
        [MXSDKOptions.sharedInstance.profiler stopMeasuringTaskWithProfile:notificationTaskProfile];
        notificationTaskProfile = nil;
    }
    // The legacy super implementation of `stopActivityIndicator` contains a number of checks grouped under `canStopActivityIndicator`
    // to determine whether the indicator can be stopped or not (and the method should thus rather be called `stopActivityIndicatorIfPossible`).
    // Since the newer indicators are not calling super implementation, the check for `canStopActivityIndicator` has to be performed manually.
    if ([self canStopActivityIndicator]) {
        [self stopLoadingUserIndicator];
    }
}

- (void)stopLoadingUserIndicator
{
    [self.delegate roomViewControllerDidStopLoading:self];
}

- (void)displayRoom:(MXKRoomDataSource *)dataSource
{
    // Remove potential preview Data
    if (roomPreviewData)
    {
        roomPreviewData = nil;
        [self removeMatrixSession:self.mainSession];
    }
    
    // Set potential discussion target user to nil, now use the dataSource to populate the view
    self.directChatTargetUser = nil;
    
    // Enable the read marker display, and disable its update.
    dataSource.showReadMarker = YES;
    self.updateRoomReadMarker = NO;
    
    [super displayRoom:dataSource];
    
    self.customizedRoomDataSource = nil;
    
    if (self.roomDataSource)
    {
        [self listenToServerNotices];
        
        self.eventsAcknowledgementEnabled = YES;
        
        // Store ref on customized room data source
        if ([dataSource isKindOfClass:RoomDataSource.class])
        {
            self.customizedRoomDataSource = (RoomDataSource*)dataSource;
        }
        
        // Set room title view
        [self refreshRoomTitle];
        
        // Stop any pending voice broadcast if needed
        [self stopUncompletedVoiceBroadcastIfNeeded];
    }
    else
    {
        self.navigationItem.rightBarButtonItem.enabled = NO;
    }
    
    [self refreshRoomInputToolbar];
    
    [VoiceMessageMediaServiceProvider.sharedProvider setCurrentRoomSummary:dataSource.room.summary];
    _voiceMessageController.roomId = dataSource.roomId;
    
    _completionSuggestionCoordinator = [[CompletionSuggestionCoordinatorBridge alloc] initWithMediaManager:self.roomDataSource.mxSession.mediaManager
                                                                                          room:dataSource.room
                                                                                        userID:self.roomDataSource.mxSession.myUserId];
    _completionSuggestionCoordinator.delegate = self;
    
    [self setupCompletionSuggestionViewIfNeeded];

    [self updateRoomInputToolbarViewClassIfNeeded];
    
    [self updateTopBanners];
}

- (void)onRoomDataSourceReady
{
    // Handle here invitation
    if (self.roomDataSource.room.summary.membership == MXMembershipInvite)
    {
        self.navigationItem.rightBarButtonItem.enabled = NO;
        
        // Show preview header
        [self showPreviewHeader:YES];
    }
    
    [super onRoomDataSourceReady];
}

- (void)updateViewControllerAppearanceOnRoomDataSourceState
{
    [super updateViewControllerAppearanceOnRoomDataSourceState];
    
    if (self.isRoomPreview)
    {
        self.navigationItem.rightBarButtonItem.enabled = NO;
        
        // Remove input tool bar if any
        if (self.inputToolbarView)
        {
            [super setRoomInputToolbarViewClass:nil];
        }
        
        if (previewHeader)
        {
            previewHeader.mxRoom = self.roomDataSource.room;
            
            // Force the layout of subviews (some constraints may have been updated)
            [self forceLayoutRefresh];
        }
    }
    else if (self.isNewDirectChat)
    {
        [self refreshRoomInputToolbar];
    }
    else
    {
        [self showPreviewHeader:NO];
        
        self.navigationItem.rightBarButtonItem.enabled = (self.roomDataSource != nil);
        
        self.titleView.editable = NO;
        
        if (self.roomDataSource)
        {
            // Update the input toolbar class and update the layout
            [self updateRoomInputToolbarViewClassIfNeeded];
            
            self.inputToolbarView.hidden = (self.roomDataSource.state != MXKDataSourceStateReady);
            
            // Restore room activities view if none
            if (!self.activitiesView)
            {
                // And the extra area
                [self setRoomActivitiesViewClass:RoomActivitiesView.class];
            }
        }
    }
}

- (void)leaveRoomOnEvent:(MXEvent*)event
{
    // Force a simple title view initialised with the current room before leaving actually the room.
    [self setRoomTitleViewClass:SimpleRoomTitleView.class];
    self.titleView.editable = NO;
    self.titleView.mxRoom = self.roomDataSource.room;
    
    // Hide the potential read marker banner.
    self.jumpToLastUnreadBannerContainer.hidden = YES;
    
    [super leaveRoomOnEvent:event];
    [self notifyDelegateOnLeaveRoomIfNecessary];
}


+ (Class) mainToolbarClass
{
    if (RiotSettings.shared.enableWysiwygComposer)
    {
        return WysiwygInputToolbarView.class;
    }
    else
    {
        return RoomInputToolbarView.class;
    }
}

// Set the input toolbar according to the current display
- (void)updateRoomInputToolbarViewClassIfNeeded
{
    Class roomInputToolbarViewClass = [RoomViewController mainToolbarClass];

    // If RTE is enabled, delay the toolbar setup until `completionSuggestionCoordinator` is ready.
    if (roomInputToolbarViewClass == WysiwygInputToolbarView.class && _completionSuggestionCoordinator == nil)
    {
        return;
    }
    
    BOOL shouldDismissContextualMenu = NO;
    
    // Check the user has enough power to post message
    if (self.roomDataSource.roomState)
    {
        MXRoomPowerLevels *powerLevels = self.roomDataSource.roomState.powerLevels;
        NSInteger userPowerLevel = [self.roomDataSource.roomState powerLevelOfUserWithUserID:self.mainSession.myUser.userId];
        
        BOOL canSend = (userPowerLevel >= [powerLevels minimumPowerLevelForSendingEventAsMessage:kMXEventTypeStringRoomMessage]);
        BOOL isRoomObsolete = self.roomDataSource.roomState.isObsolete;
        BOOL isResourceLimitExceeded = [self.roomDataSource.mxSession.syncError.errcode isEqualToString:kMXErrCodeStringResourceLimitExceeded];        
        
        if (isRoomObsolete || isResourceLimitExceeded || _isWaitingForOtherParticipants)
        {
            roomInputToolbarViewClass = nil;
            shouldDismissContextualMenu = YES;
        }
        else if (!canSend)
        {
            roomInputToolbarViewClass = DisabledRoomInputToolbarView.class;
            shouldDismissContextualMenu = YES;
        }
    }
    
    // Do not show toolbar in case of preview
    if (self.isRoomPreview)
    {
        roomInputToolbarViewClass = nil;
        shouldDismissContextualMenu = YES;
    }
    
    if (shouldDismissContextualMenu)
    {
        [self hideContextualMenuAnimated:NO];
    }
    
    // Change inputToolbarView class only if given class is different from current one
    if (!self.inputToolbarView || ![self.inputToolbarView isMemberOfClass:roomInputToolbarViewClass])
    {
        [super setRoomInputToolbarViewClass:roomInputToolbarViewClass];
        if ([self.inputToolbarView.class conformsToProtocol:@protocol(RoomInputToolbarViewProtocol)]) {
            id<RoomInputToolbarViewProtocol> inputToolbar = (id<RoomInputToolbarViewProtocol>)self.inputToolbarView;
            [inputToolbar setVoiceMessageToolbarView:self.voiceMessageController.voiceMessageToolbarView];
        }
        
        [self updateInputToolBarViewHeight];
        [self refreshRoomInputToolbar];
    }
}

// Get the height of the current room input toolbar
- (CGFloat)inputToolbarHeight
{
    CGFloat height = 0;
    
    if ([self.inputToolbarView.class conformsToProtocol:@protocol(RoomInputToolbarViewProtocol)]) {
        id<RoomInputToolbarViewProtocol> inputToolbar = (id<RoomInputToolbarViewProtocol>)self.inputToolbarView;
        height = inputToolbar.toolbarHeight;
    }
    else if ([self.inputToolbarView isKindOfClass:DisabledRoomInputToolbarView.class])
    {
        height = ((DisabledRoomInputToolbarView*)self.inputToolbarView).mainToolbarMinHeightConstraint.constant;
    }
    
    return height;
}

- (void)setRoomActivitiesViewClass:(Class)roomActivitiesViewClass
{
    // Do not show room activities in case of preview (FIXME: show it when live events will be supported during peeking)
    if (self.isRoomPreview)
    {
        roomActivitiesViewClass = nil;
    }
    
    [super setRoomActivitiesViewClass:roomActivitiesViewClass];
    
    if (!self.isActivitiesViewExpanded)
    {
        self.roomActivitiesContainerHeightConstraint.constant = 0;
    }
}

- (BOOL)sendAsIRCStyleCommandIfPossible:(NSString*)string
{
    // Override the default behavior for `/join` command in order to open automatically the joined room

    NSString* kMXKSlashCmdJoinRoom = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandJoinRoom];
    
    if ([string hasPrefix:kMXKSlashCmdJoinRoom])
    {
        // Join a room
        NSString *roomAlias;
        
        // Sanity check
        if (string.length > kMXKSlashCmdJoinRoom.length)
        {
            roomAlias = [string substringFromIndex:kMXKSlashCmdJoinRoom.length + 1];
            
            // Remove white space from both ends
            roomAlias = [roomAlias stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
        }
        
        // Check
        if (roomAlias.length)
        {
            Analytics.shared.joinedRoomTrigger = AnalyticsJoinedRoomTriggerSlashCommand;
            
            // TODO: /join command does not support via parameters yet
            [self.mainSession joinRoom:roomAlias viaServers:nil success:^(MXRoom *room) {
                                
                [self showRoomWithId:room.roomId];
                
            } failure:^(NSError *error) {
                
                MXLogDebug(@"[RoomVC] Join roomAlias (%@) failed", roomAlias);
                //Alert user
                [self showError:error];
                
            }];
        }
        else
        {
            // Display cmd usage in text input as placeholder
            self.inputToolbarView.placeholder = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandJoinRoom];
        }
        return YES;
    }
    return [super sendAsIRCStyleCommandIfPossible:string];
}

- (void)setKeyboardHeight:(CGFloat)keyboardHeight
{
    [super setKeyboardHeight:keyboardHeight];

    self.inputToolbarView.maxHeight = round(([UIScreen mainScreen].bounds.size.height - keyboardHeight) * 0.7);

    // Make the activity indicator follow the keyboard
    // At runtime, this creates a smooth animation
    CGPoint activityIndicatorCenter = self.activityIndicator.center;
    activityIndicatorCenter.y = self.view.center.y - keyboardHeight / 2;
    self.activityIndicator.center = activityIndicatorCenter;
}

- (void)dismissTemporarySubViews
{
    [super dismissTemporarySubViews];
    
    if (encryptionInfoView)
    {
        [encryptionInfoView removeFromSuperview];
        encryptionInfoView = nil;
    }
}

- (void)setBubbleTableViewDisplayInTransition:(BOOL)bubbleTableViewDisplayInTransition
{
    if (self.isBubbleTableViewDisplayInTransition != bubbleTableViewDisplayInTransition)
    {
        [super setBubbleTableViewDisplayInTransition:bubbleTableViewDisplayInTransition];
        
        // Refresh additional displays when the table is ready.
        if (!bubbleTableViewDisplayInTransition && !self.bubblesTableView.isHidden)
        {
            [self refreshActivitiesViewDisplay];
            [self refreshRoomTitle];
            
            [self checkReadMarkerVisibility];
            [self refreshJumpToLastUnreadBannerDisplay];
        }
    }
}

- (void)sendTextMessage:(NSString*)msgTxt
{
    // Create before sending the message in case of a discussion (direct chat)
    MXWeakify(self);
    [self createDiscussionIfNeeded:^(BOOL readyToSend) {
        MXStrongifyAndReturnIfNil(self);
        if (readyToSend)
        {
            // The event modified is always fetch from the actual data source
            MXEvent *eventModified = [self.roomDataSource eventWithEventId:self.customizedRoomDataSource.selectedEventId];
            
            // In the case the event is a reply or and edit, and it's done on a non-live timeline
            // we have to fetch live timeline in order to display the event properly
            [self setupRoomDataSourceToResolveEvent:^(MXKRoomDataSource *roomDataSource) {
                if (self.inputToolBarSendMode == RoomInputToolbarViewSendModeReply && eventModified)
                {
                    [roomDataSource sendReplyToEvent:eventModified withTextMessage:msgTxt success:nil failure:^(NSError *error) {
                        // Just log the error. The message will be displayed in red in the room history
                        MXLogDebug(@"[MXKRoomViewController] sendTextMessage failed.");
                    }];
                }
                else if (self.inputToolBarSendMode == RoomInputToolbarViewSendModeEdit && eventModified)
                {
                    [roomDataSource replaceTextMessageForEvent:eventModified withTextMessage:msgTxt success:nil failure:^(NSError *error) {
                        // Just log the error. The message will be displayed in red
                        MXLogDebug(@"[MXKRoomViewController] sendTextMessage failed.");
                    }];
                }
                else
                {
                    // Let the datasource send it and manage the local echo
                    [roomDataSource sendTextMessage:msgTxt success:nil failure:^(NSError *error)
                     {
                        // Just log the error. The message will be displayed in red in the room history
                        MXLogDebug(@"[MXKRoomViewController] sendTextMessage failed.");
                    }];
                }
                
                if (self.customizedRoomDataSource.selectedEventId)
                {
                    [self cancelEventSelection];
                }
            }];
        }
        // Errors are handled at the request level. This should be improved in case of code rewriting.
    }];
}

- (void)setupRoomDataSourceToResolveEvent: (void (^)(MXKRoomDataSource *roomDataSource))onComplete
{
    // If the event occur on timeline not live, use the live data source to resolve event
    BOOL isLive = self.roomDataSource.isLive;
    if (!isLive)
    {
        if (self.roomDataSourceLive == nil)
        {
            MXKRoomDataSourceManager *roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:self.mainSession];

            [roomDataSourceManager roomDataSourceForRoom:self.roomDataSource.roomId
                                                  create:YES
                                              onComplete:^(MXKRoomDataSource *roomDataSource) {
                self.roomDataSourceLive = roomDataSource;
                [self.roomDataSourceLive finalizeInitialization];
                onComplete(self.roomDataSourceLive);
            }];
        }
        else
        {
            onComplete(self.roomDataSourceLive);
        }
    }
    else
    {
        onComplete(self.roomDataSource);
    }
}

- (void)setRoomTitleViewClass:(Class)roomTitleViewClass
{
    if ([self.titleView.class isEqual:roomTitleViewClass]) {
        return;
    }
    
    // Sanity check: accept only MXKRoomTitleView classes or sub-classes
    NSParameterAssert([roomTitleViewClass isSubclassOfClass:MXKRoomTitleView.class]);
    
    MXKRoomTitleView *titleView = [roomTitleViewClass roomTitleView];
    [self setValue:titleView forKey:@"titleView"];
    titleView.delegate = self;
    titleView.mxRoom = self.roomDataSource.room;
    titleView.mxUser = self.directChatTargetUser;
    self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:titleView];
    
    if ([titleView isKindOfClass:RoomTitleView.class])
    {
        RoomTitleView *roomTitleView = (RoomTitleView*)self.titleView;
        missedDiscussionsBadgeLabel = roomTitleView.missedDiscussionsBadgeLabel;
        missedDiscussionsDotView = roomTitleView.dotView;
        [roomTitleView updateLayoutForOrientation:[UIApplication sharedApplication].statusBarOrientation];
    }

    [self updateViewControllerAppearanceOnRoomDataSourceState];
    
    [self updateTitleViewEncryptionDecoration];
}

- (void)destroy
{
    if (currentAlert)
    {
        [currentAlert dismissViewControllerAnimated:NO completion:nil];
        currentAlert = nil;
    }
    
    if (self.customizedRoomDataSource)
    {
        self.customizedRoomDataSource.selectedEventId = nil;
        self.customizedRoomDataSource = nil;
    }
    
    [self removeTypingNotificationsListener];
    
    if (kThemeServiceDidChangeThemeNotificationObserver)
    {
        [[NSNotificationCenter defaultCenter] removeObserver:kThemeServiceDidChangeThemeNotificationObserver];
        kThemeServiceDidChangeThemeNotificationObserver = nil;
    }
    if (kAppDelegateDidTapStatusBarNotificationObserver)
    {
        [[NSNotificationCenter defaultCenter] removeObserver:kAppDelegateDidTapStatusBarNotificationObserver];
        kAppDelegateDidTapStatusBarNotificationObserver = nil;
    }
    if (kAppDelegateNetworkStatusDidChangeNotificationObserver)
    {
        [[NSNotificationCenter defaultCenter] removeObserver:kAppDelegateNetworkStatusDidChangeNotificationObserver];
        kAppDelegateNetworkStatusDidChangeNotificationObserver = nil;
    }
    if (mxRoomSummaryDidChangeObserver)
    {
        [[NSNotificationCenter defaultCenter] removeObserver:mxRoomSummaryDidChangeObserver];
        mxRoomSummaryDidChangeObserver = nil;
    }
    if (mxEventDidDecryptNotificationObserver)
    {
        [[NSNotificationCenter defaultCenter] removeObserver:mxEventDidDecryptNotificationObserver];
        mxEventDidDecryptNotificationObserver = nil;
    }
    if (URLPreviewDidUpdateNotificationObserver)
    {
        [NSNotificationCenter.defaultCenter removeObserver:URLPreviewDidUpdateNotificationObserver];        
    }
    
    [self removeCallNotificationsListeners];
    [self removeWidgetNotificationsListeners];
    [self removeTombstoneEventNotificationsListener];
    [self removeMXSessionStateChangeNotificationsListener];
    [self removeServerNoticesListener];
    
    if (previewHeader)
    {
        // Here [destroy] is called before [viewWillDisappear:]
        MXLogDebug(@"[RoomVC] destroyed whereas it is still visible");
        
        [previewHeader removeFromSuperview];
        previewHeader = nil;
        
        // Hide preview header container to ignore [self showPreviewHeader:NO] call (if any).
        self.previewHeaderContainer.hidden = YES;
    }
    
    roomPreviewData = nil;
    
    missedDiscussionsBadgeLabel = nil;
    
    [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXEventDidChangeSentStateNotification object:nil];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXEventDidChangeIdentifierNotification object:nil];
    
    [self waitForOtherParticipant:NO];
    
    [super destroy];
}

#pragma mark - Start DM

/**
 Create a direct chat with given user.
 */
- (void)createDiscussionWithUser:(MXUser*)user completion:(void (^)(BOOL success))onComplete
{
    [self startActivityIndicator];
    
    [[AppDelegate theDelegate] createDirectChatWithUserId:user.userId completion:^(NSString *roomId) {
        if (roomId)
        {
            MXKRoomDataSourceManager *roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:self.mainSession];
            [roomDataSourceManager roomDataSourceForRoom:roomId create:YES onComplete:^(MXKRoomDataSource *roomDataSource) {
                [self stopActivityIndicator];
                [self setRoomInputToolbarViewClass:nil];
                [self displayRoom:roomDataSource];
                
                onComplete(YES);
            }];
        }
        else
        {
            [self stopActivityIndicator];
            onComplete(NO);
        }
    }];
}

/**
 Create the discussion if needed
 */
- (void)createDiscussionIfNeeded:(void (^)(BOOL readyToSend))onComplete
{
    void(^completion)(BOOL) = ^(BOOL readyToSend) {
        self.inputToolbarView.userInteractionEnabled = true;
        if (onComplete) {
            onComplete(readyToSend);
        }
    };
    
    if (self.directChatTargetUser)
    {
        // Disable the input tool bar during this operation. This prevents us from creating several discussions, or
        // trying to send several invites.
        self.inputToolbarView.userInteractionEnabled = false;
        
        [self createDiscussionWithUser:self.directChatTargetUser completion:completion];
    }
    else
    {
        completion(YES);
    }
}

#pragma mark - Properties

-(void)setActivitiesViewExpanded:(BOOL)activitiesViewExpanded
{
    if (_activitiesViewExpanded != activitiesViewExpanded)
    {
        _activitiesViewExpanded = activitiesViewExpanded;
        
        self.roomActivitiesContainerHeightConstraint.constant = activitiesViewExpanded ? 53 : 0;
        [super roomInputToolbarView:self.inputToolbarView heightDidChanged:[self inputToolbarHeight] completion:nil];
    }
}

- (void)setScrollToBottomHidden:(BOOL)scrollToBottomHidden
{
    if (_scrollToBottomHidden != scrollToBottomHidden)
    {
        _scrollToBottomHidden = scrollToBottomHidden;
    }
    
    if (!_scrollToBottomHidden && [self.roomDataSource isKindOfClass:RoomDataSource.class])
    {
        RoomDataSource *roomDataSource = (RoomDataSource *) self.roomDataSource;
        if (roomDataSource.currentTypingUsers && !roomDataSource.currentTypingUsers.count)
        {
            [roomDataSource resetTypingNotification];
            [self.bubblesTableView reloadData];
        }
    }

    [UIView animateWithDuration:.2 animations:^{
        self.scrollToBottomBadgeLabel.alpha = (scrollToBottomHidden || !self.scrollToBottomBadgeLabel.text) ? 0 : 1;
        self.scrollToBottomButton.alpha = scrollToBottomHidden ? 0 : 1;
    }];
}

- (void)setMissedDiscussionsBadgeHidden:(BOOL)missedDiscussionsBadgeHidden{
    _missedDiscussionsBadgeHidden = missedDiscussionsBadgeHidden;
    
    missedDiscussionsBadgeLabel.hidden = missedDiscussionsBadgeHidden;
    missedDiscussionsDotView.hidden = missedDiscussionsBadgeHidden;
}

- (BOOL)shouldShowLiveLocationSharingBannerView
{
    return self.customizedRoomDataSource.isCurrentUserSharingActiveLocation;
}

#pragma mark - Wait for 3rd party invitee

- (void)setIsWaitingForOtherParticipants:(BOOL)isWaitingForOtherParticipants
{
    if (_isWaitingForOtherParticipants == isWaitingForOtherParticipants)
    {
        return;
    }

    _isWaitingForOtherParticipants = isWaitingForOtherParticipants;
    [self updateRoomInputToolbarViewClassIfNeeded];
    
    if (_isWaitingForOtherParticipants)
    {
        if (self->roomMemberEventListener == nil)
        {
            MXWeakify(self);
            self->roomMemberEventListener = [self.roomDataSource.room listenToEventsOfTypes:@[kMXEventTypeStringRoomMember] onEvent:^(MXEvent *event, MXTimelineDirection direction, MXRoomState *roomState) {
                MXStrongifyAndReturnIfNil(self);
                if (direction != MXTimelineDirectionForwards)
                {
                    return;
                }
                [self refreshWaitForOtherParticipantsState];
            }];
        }
    }
    else
    {
        if (self->roomMemberEventListener != nil)
        {
            [self.roomDataSource.room removeListener:self->roomMemberEventListener];
            self->roomMemberEventListener = nil;
        }
    }
}

- (BOOL)shouldWaitForOtherParticipants
{
    MXRoomState *roomState = self.roomDataSource.roomState;
    BOOL isDirect = self.roomDataSource.room.isDirect;
    
    // Wait for the other participant only if it is a direct encrypted room with only one member waiting for a third party guest.
    return (isDirect && roomState.isEncrypted && roomState.membersCount.members == 1 && roomState.thirdPartyInvites.count > 0);
}

- (void)refreshWaitForOtherParticipantsState
{
    [self waitForOtherParticipant:self.shouldWaitForOtherParticipants];
}

#pragma mark - Internals

- (UIBarButtonItem *)videoCallBarButtonItem
{
    UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithImage:AssetImages.videoCall.image
                                                             style:UIBarButtonItemStylePlain
                                                            target:self
                                                            action:@selector(onVideoCallPressed:)];
    item.accessibilityLabel = [VectorL10n roomAccessibilityVideoCall];
    
    return item;
}

- (UIBarButtonItem *)joinJitsiBarButtonItem
{
    CallTileActionButton *button = [CallTileActionButton new];
    [button setImage:AssetImages.callVideoIcon.image
            forState:UIControlStateNormal];
    [button setTitle:[VectorL10n roomJoinGroupCall]
            forState:UIControlStateNormal];
    [button addTarget:self
               action:@selector(onVideoCallPressed:)
     forControlEvents:UIControlEventTouchUpInside];
    button.contentEdgeInsets = UIEdgeInsetsMake(4, 12, 4, 12);
    
    UIBarButtonItem *item;
    
    if (RiotSettings.shared.enableThreads)
    {
        // Add some spacing when there is a threads button
        UIView *buttonContainer = [[UIView alloc] initWithFrame:CGRectZero];
        [buttonContainer vc_addSubViewMatchingParent:button withInsets:UIEdgeInsetsMake(0, 0, 0, -12)];
        
        item = [[UIBarButtonItem alloc] initWithCustomView:buttonContainer];
    }
    else
    {
        item = [[UIBarButtonItem alloc] initWithCustomView:button];
    }
    
    item.accessibilityLabel = [VectorL10n roomAccessibilityVideoCall];
    
    return item;
}

- (UIBarButtonItem *)threadMoreBarButtonItem
{
    UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithImage:AssetImages.roomContextMenuMore.image
                                                             style:UIBarButtonItemStylePlain
                                                            target:self
                                                            action:@selector(onButtonPressed:)];
    item.accessibilityLabel = [VectorL10n roomAccessibilityThreadMore];
    
    return item;
}

- (UIBarButtonItem *)threadListBarButtonItem
{
    UIButton *button = [UIButton new];
    button.contentEdgeInsets = kThreadListBarButtonItemContentInsetsNoDot;
    button.imageView.contentMode = UIViewContentModeScaleAspectFit;
    [button setImage:[AssetImages.threadsIcon.image vc_resizedWith:kThreadListBarButtonItemImageSize]
            forState:UIControlStateNormal];
    [button addTarget:self
               action:@selector(onThreadListTapped:)
     forControlEvents:UIControlEventTouchUpInside];
    button.accessibilityLabel = [VectorL10n roomAccessibilityThreads];

    UIBarButtonItem *result = [[UIBarButtonItem alloc] initWithCustomView:button];
    result.tag = kThreadListBarButtonItemTag;
    return result;
}

- (void)setupRemoveJitsiWidgetRemoveView
{
    if (!self.displayConfiguration.jitsiWidgetRemoverEnabled)
    {
        return;
    }
    
    self.removeJitsiWidgetView = [RemoveJitsiWidgetView instantiate];
    self.removeJitsiWidgetView.delegate = self;
    
    [self.removeJitsiWidgetContainer vc_addSubViewMatchingParent:self.removeJitsiWidgetView];
    
    self.removeJitsiWidgetContainer.hidden = YES;
    
    [self refreshRemoveJitsiWidgetView];
}

- (void)forceLayoutRefresh
{
    // Sanity check: check whether the table view data source is set.
    if (self.bubblesTableView.dataSource)
    {
        [self.view layoutIfNeeded];
    }
}

- (BOOL)isRoomPreview
{
    if (self.isContextPreview)
    {
        return YES;
    }
    
    // Check first whether some preview data are defined.
    if (roomPreviewData)
    {
        return YES;
    }
    
    if (self.roomDataSource && self.roomDataSource.state == MXKDataSourceStateReady && self.roomDataSource.room.summary.membership == MXMembershipInvite)
    {
        return YES;
    }
    
    return NO;
}

// Indicates if a new direct chat with a target user (without associated room) is occuring.
- (BOOL)isNewDirectChat
{
    return self.directChatTargetUser != nil;
}

- (BOOL)isEncryptionEnabled
{
    return self.roomDataSource.room.summary.isEncrypted && self.mainSession.crypto != nil;
}

- (BOOL)supportCallOption
{
    if (!self.displayConfiguration.callsEnabled)
    {
        return NO;
    }
    BOOL callOptionAllowed = (self.roomDataSource.room.isDirect && RiotSettings.shared.roomScreenAllowVoIPForDirectRoom) || (!self.roomDataSource.room.isDirect && RiotSettings.shared.roomScreenAllowVoIPForNonDirectRoom);
    return callOptionAllowed && BuildSettings.allowVoIPUsage && self.roomDataSource.mxSession.callManager && self.roomDataSource.room.summary.membersCount.joined >= 2;
}

- (BOOL)isCallActive
{
    MXCall *callInRoom = [self.roomDataSource.mxSession.callManager callInRoom:self.roomDataSource.roomId];
    
    return (callInRoom && callInRoom.state != MXCallStateEnded)
    || self.customizedRoomDataSource.jitsiWidget;
}

- (BOOL)canSendStateEventWithType:(MXEventTypeString)eventTypeString
{
    MXRoomPowerLevels *powerLevels = [self.roomDataSource.roomState powerLevels];
    NSInteger requiredPower = [powerLevels minimumPowerLevelForSendingEventAsStateEvent:eventTypeString];
    NSInteger myPower = [self.roomDataSource.roomState powerLevelOfUserWithUserID:self.roomDataSource.mxSession.myUserId];
    return myPower >= requiredPower;
}

/**
 Returns a flag for the current user whether it's privileged to add/remove Jitsi widgets to this room.
 */
- (BOOL)canEditJitsiWidget
{
    return [self canSendStateEventWithType:kWidgetModularEventTypeString];
}

- (void)registerURLPreviewNotifications
{
    MXWeakify(self);
    
    URLPreviewDidUpdateNotificationObserver = [NSNotificationCenter.defaultCenter addObserverForName:URLPreviewDidUpdateNotification object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification * _Nonnull notification) {
        
        MXStrongifyAndReturnIfNil(self);        
        
        // Ensure this is the correct room
        if (![(NSString*)notification.userInfo[@"roomId"] isEqualToString:self.roomDataSource.roomId])
        {
            return;
        }
        
        // Get the indexPath for the updated cell.
        NSString *updatedEventId = notification.userInfo[@"eventId"];
        NSInteger updatedEventIndex = [self.roomDataSource indexOfCellDataWithEventId:updatedEventId];
        NSIndexPath *updatedIndexPath = [NSIndexPath indexPathForRow:updatedEventIndex inSection:0];
        
        // Store the content size and offset before reloading the cell
        CGFloat originalContentSize = self.bubblesTableView.contentSize.height;
        CGPoint contentOffset = self.bubblesTableView.contentOffset;
        
        // Only update the content offset if the cell is visible or above the current visible cells.
        BOOL shouldUpdateContentOffset = NO;
        NSIndexPath *lastVisibleIndexPath = [self.bubblesTableView indexPathsForVisibleRows].lastObject;
        if (lastVisibleIndexPath && updatedIndexPath.row < lastVisibleIndexPath.row)
        {
            shouldUpdateContentOffset = YES;
        }
        
        // Note: Despite passing in the index path, this reloads the whole table.
        [self dataSource:self.roomDataSource didCellChange:updatedIndexPath];
        
        // Update the content offset to include any changes to the scroll view's height.
        if (shouldUpdateContentOffset)
        {
            CGFloat delta = self.bubblesTableView.contentSize.height - originalContentSize;
            contentOffset.y += delta;
            
            self.bubblesTableView.contentOffset = contentOffset;
        }
    }];
}

- (void)refreshRoomTitle
{
    NSMutableArray *rightBarButtonItems = nil;
    
    // Set the right room title view
    if (self.isRoomPreview)
    {
        [self showPreviewHeader:YES];
    }
    else if (self.roomDataSource)
    {
        [self showPreviewHeader:NO];
        
        if (self.roomDataSource.isLive)
        {
            rightBarButtonItems = [NSMutableArray new];
            BOOL hasCustomJoinButton = NO;
            
            if (self.supportCallOption)
            {
                if (self.roomDataSource.room.summary.membersCount.joined == 2
                    && self.roomDataSource.room.isDirect
                    && !self.mainSession.vc_homeserverConfiguration.jitsi.useFor1To1Calls)
                {
                    //  voice call button for Matrix call
                    UIBarButtonItem *itemVoice = [[UIBarButtonItem alloc] initWithImage:AssetImages.voiceCallHangonIcon.image
                                                                                  style:UIBarButtonItemStylePlain
                                                                                 target:self
                                                                                 action:@selector(onVoiceCallPressed:)];
                    itemVoice.accessibilityLabel = [VectorL10n roomAccessibilityCall];
                    itemVoice.enabled = !self.isCallActive;
                    [rightBarButtonItems addObject:itemVoice];
                    
                    //  video call button for Matrix call
                    UIBarButtonItem *itemVideo = [self videoCallBarButtonItem];
                    itemVideo.enabled = !self.isCallActive;
                    [rightBarButtonItems addObject:itemVideo];
                }
                else
                {
                    //  video call button for Jitsi call
                    if (self.isCallActive)
                    {
                        if (self.isRoomHavingAJitsiCall)
                        {
                            //  show a disabled call button
                            UIBarButtonItem *item = [self videoCallBarButtonItem];
                            item.enabled = NO;
                            [rightBarButtonItems addObject:item];
                        }
                        else
                        {
                            UIBarButtonItem *item = [self joinJitsiBarButtonItem];
                            [rightBarButtonItems addObject:item];
                            
                            hasCustomJoinButton = YES;
                        }
                    }
                    else
                    {
                        //  show a video call button
                        //  item will still be enabled, and when tapped an alert will be displayed to the user
                        UIBarButtonItem *item = [self videoCallBarButtonItem];
                        if (!self.canEditJitsiWidget)
                        {
                            item.image = [AssetImages.videoCall.image vc_withAlpha:0.3];
                        }
                        [rightBarButtonItems addObject:item];
                    }
                }
            }
            
            if ([self widgetsCount:NO])
            {
                UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithImage:AssetImages.integrationsIcon.image
                                                                         style:UIBarButtonItemStylePlain
                                                                        target:self
                                                                        action:@selector(onIntegrationsPressed:)];
                item.accessibilityLabel = [VectorL10n roomAccessibilityIntegrations];
                if (hasCustomJoinButton)
                {
                    item.imageInsets = UIEdgeInsetsMake(0, -5, 0, -5);
                    item.landscapeImagePhoneInsets = UIEdgeInsetsMake(0, -5, 0, -5);
                }
                [rightBarButtonItems addObject:item];
            }
        }
        
        // Do not change title view class here if the expanded header is visible.
        [self setRoomTitleViewClass:RoomTitleView.class];
        ((RoomTitleView*)self.titleView).tapGestureDelegate = self;
        
        MXKImageView *userPictureView = ((RoomTitleView*)self.titleView).pictureView;
        
        // Set user picture in input toolbar
        if (userPictureView)
        {
            [self.roomDataSource.room.summary setRoomAvatarImageIn:userPictureView];
        }
        
        [self refreshMissedDiscussionsCount:YES];
        
        if (RiotSettings.shared.enableThreads && !_isWaitingForOtherParticipants)
        {
            if (self.roomDataSource.threadId)
            {
                //  in a thread
                if (rightBarButtonItems == nil)
                {
                    rightBarButtonItems = [NSMutableArray new];
                }
                UIBarButtonItem *itemThreadMore = [self threadMoreBarButtonItem];
                [rightBarButtonItems insertObject:itemThreadMore atIndex:0];
            }
            else
            {
                //  in a regular timeline
                UIBarButtonItem *itemThreadList = [self threadListBarButtonItem];
                [self updateThreadListBarButtonItem:itemThreadList
                                               with:self.mainSession.threadingService];
                [rightBarButtonItems insertObject:itemThreadList atIndex:0];
            }
        }
    }
    else if (self.isNewDirectChat)
    {
        [self showPreviewHeader:NO];
        
        [self setRoomTitleViewClass:RoomTitleView.class];
        MXKImageView *userPictureView = ((RoomTitleView*)self.titleView).pictureView;
        
        // Set user picture in input toolbar
        if (userPictureView)
        {
            [userPictureView vc_setRoomAvatarImageWith:self.directChatTargetUser.avatarUrl
                                                roomId:self.directChatTargetUser.userId
                                           displayName:self.directChatTargetUser.displayname ?: self.directChatTargetUser.userId
                                          mediaManager:self.mainSession.mediaManager];
        }
    }
    
    self.navigationItem.rightBarButtonItems = rightBarButtonItems;
}

- (void)updateInputToolBarVisibility
{
    BOOL hideInputToolBar = NO;
    
    if (self.roomDataSource)
    {
        hideInputToolBar = (self.roomDataSource.state != MXKDataSourceStateReady);
    }
    
    self.inputToolbarView.hidden = hideInputToolBar;
}

- (void)refreshRoomInputToolbar
{
    MXKImageView *userPictureView;
    
    // Show or hide input tool bar
    [self updateInputToolBarVisibility];
    
    // Check whether the input toolbar is ready before updating it.
    if (self.inputToolbarView && [self inputToolbarConformsToToolbarViewProtocol])
    {
        id<RoomInputToolbarViewProtocol> roomInputToolbarView = (id<RoomInputToolbarViewProtocol>) self.inputToolbarView;
        
        // Update encryption decoration if needed
        [self updateEncryptionDecorationForRoomInputToolbar:roomInputToolbarView];

        // Update actions when the input toolbar refreshed
        [self setupActions];
        
        // Update placeholder and hide voice message view
        if (self.isNewDirectChat)
        {
            [self setInputToolBarSendMode:RoomInputToolbarViewSendModeCreateDM forEventWithId:nil];
            [roomInputToolbarView setVoiceMessageToolbarView:nil];
        }
    }
    else if (self.inputToolbarView && [self.inputToolbarView isKindOfClass:DisabledRoomInputToolbarView.class])
    {
        DisabledRoomInputToolbarView *roomInputToolbarView = (DisabledRoomInputToolbarView*)self.inputToolbarView;
        
        // Get user picture view in input toolbar
        userPictureView = roomInputToolbarView.pictureView;
        
        // For the moment, there is only one reason to use `DisabledRoomInputToolbarView`
        [roomInputToolbarView setDisabledReason:[VectorL10n roomDoNotHavePermissionToPost]];
    }
    
    // Set user picture in input toolbar
    if (userPictureView)
    {
        UIImage *preview = [AvatarGenerator generateAvatarForMatrixItem:self.mainSession.myUser.userId withDisplayName:self.mainSession.myUser.displayname];
        
        // Suppose the avatar is stored unencrypted on the Matrix media repository.
        userPictureView.enableInMemoryCache = YES;
        [userPictureView setImageURI:self.mainSession.myUser.avatarUrl
                            withType:nil
                 andImageOrientation:UIImageOrientationUp
                       toFitViewSize:userPictureView.frame.size
                          withMethod:MXThumbnailingMethodCrop
                        previewImage:preview
                        mediaManager:self.mainSession.mediaManager];
        [userPictureView.layer setCornerRadius:userPictureView.frame.size.width / 2];
        userPictureView.clipsToBounds = YES;
    }
}

- (void)setInputToolBarSendMode:(RoomInputToolbarViewSendMode)sendMode forEventWithId:(NSString *)eventId
{
    if (self.inputToolbarView && [self inputToolbarConformsToToolbarViewProtocol])
    {
        MXKRoomInputToolbarView <RoomInputToolbarViewProtocol> *roomInputToolbarView = (MXKRoomInputToolbarView <RoomInputToolbarViewProtocol> *) self.inputToolbarView;
        if (eventId)
        {
            MXEvent *event = [self.roomDataSource eventWithEventId:eventId];
            MXRoomMember * roomMember = [self.roomDataSource.roomState.members memberWithUserId:event.sender];
            if (roomMember.displayname.length)
            {
                roomInputToolbarView.eventSenderDisplayName = roomMember.displayname;
            }
            else
            {
                roomInputToolbarView.eventSenderDisplayName = event.sender;
            }
        }
        else
        {
            roomInputToolbarView.eventSenderDisplayName = nil;
        }
        roomInputToolbarView.sendMode = sendMode;
    }
}

- (RoomInputToolbarViewSendMode)inputToolBarSendMode
{
    RoomInputToolbarViewSendMode sendMode = RoomInputToolbarViewSendModeSend;
    if (self.inputToolbarView && [self.inputToolbarView isKindOfClass:[RoomInputToolbarView class]])
    {
        RoomInputToolbarView *roomInputToolbarView = (RoomInputToolbarView*)self.inputToolbarView;
        sendMode = roomInputToolbarView.sendMode;
    }
    
    return sendMode;
}

- (void)onSwipeGesture:(UISwipeGestureRecognizer*)swipeGestureRecognizer
{
    UIView *view = swipeGestureRecognizer.view;
    
    if (view == self.activitiesView)
    {
        // Dismiss the keyboard when user swipes down on activities view.
        [self.inputToolbarView dismissKeyboard];
    }
}

- (void)updateInputToolBarViewHeight
{
    // Update the inputToolBar height.
    CGFloat height = [self inputToolbarHeight];
    // Disable animation during the update
    [UIView setAnimationsEnabled:NO];
    [self roomInputToolbarView:self.inputToolbarView heightDidChanged:height completion:nil];
    [UIView setAnimationsEnabled:YES];
}

- (UIImage*)roomEncryptionBadgeImage
{
    UIImage *encryptionIcon;
    
    if (self.isEncryptionEnabled)
    {
        RoomEncryptionTrustLevel roomEncryptionTrustLevel = ((RoomDataSource*)self.roomDataSource).encryptionTrustLevel;
        
        encryptionIcon = [EncryptionTrustLevelBadgeImageHelper roomBadgeImageFor:roomEncryptionTrustLevel];
    }
    
    return encryptionIcon;
}

- (void)updateInputToolbarEncryptionDecoration
{
    if (self.inputToolbarView && [self inputToolbarConformsToToolbarViewProtocol])
    {
        id<RoomInputToolbarViewProtocol> roomInputToolbarView = (id<RoomInputToolbarViewProtocol>)self.inputToolbarView;
        [self updateEncryptionDecorationForRoomInputToolbar:roomInputToolbarView];
    }
}

- (void)updateTitleViewEncryptionDecoration
{
    if (![self.titleView isKindOfClass:[RoomTitleView class]])
    {
        return;
    }
    
    RoomTitleView *roomTitleView = (RoomTitleView*)self.titleView;
    roomTitleView.badgeImageView.image = self.roomEncryptionBadgeImage;
}

- (void)updateEncryptionDecorationForRoomInputToolbar:(id<RoomInputToolbarViewProtocol>)roomInputToolbarView
{
    roomInputToolbarView.isEncryptionEnabled = self.isEncryptionEnabled;
}

- (void)handleLongPressFromCell:(id<MXKCellRendering>)cell withTappedEvent:(MXEvent*)event
{
    if (event && !self.customizedRoomDataSource.selectedEventId)
    {
        [self showContextualMenuForEvent:event fromSingleTapGesture:NO cell:cell animated:YES];
    }
}

- (void)showReactionHistoryForEventId:(NSString*)eventId animated:(BOOL)animated
{
    if (self.reactionHistoryCoordinatorBridgePresenter.isPresenting)
    {
        return;
    }
    
    ReactionHistoryCoordinatorBridgePresenter *presenter = [[ReactionHistoryCoordinatorBridgePresenter alloc] initWithSession:self.mainSession roomId:self.roomDataSource.roomId eventId:eventId];
    presenter.delegate = self;
    
    [presenter presentFrom:self animated:animated];
    
    self.reactionHistoryCoordinatorBridgePresenter = presenter;
}

- (void)showCameraControllerAnimated:(BOOL)animated
{
    CameraPresenter *cameraPresenter = [CameraPresenter new];
    cameraPresenter.delegate = self;
    [cameraPresenter presentCameraFrom:self with:@[MXKUTI.image, MXKUTI.movie] animated:YES];
    
    self.cameraPresenter = cameraPresenter;
}


- (void)showMediaPickerAnimated:(BOOL)animated
{
    MediaPickerCoordinatorBridgePresenter *mediaPickerPresenter = [[MediaPickerCoordinatorBridgePresenter alloc] initWithSession:self.mainSession mediaUTIs:@[MXKUTI.image, MXKUTI.movie] allowsMultipleSelection:YES];
    mediaPickerPresenter.delegate = self;
    
    UIView *sourceView;
    
    if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class])
    {
        sourceView = ((RoomInputToolbarView*)self.inputToolbarView).attachMediaButton;
    }
    else
    {
        sourceView = self.inputToolbarView;
    }
    
    [mediaPickerPresenter presentFrom:self sourceView:sourceView sourceRect:sourceView.bounds animated:YES];
    
    self.mediaPickerPresenter = mediaPickerPresenter;
}

- (void)showRoomCreationModal
{
    [self.roomCreationModalCoordinatorBridgePresenter dismissWithAnimated:NO completion:nil];
    
    self.roomCreationModalCoordinatorBridgePresenter = [[RoomCreationModalCoordinatorBridgePresenter alloc] initWithSession:self.mainSession roomState:self.roomDataSource.roomState];
    self.roomCreationModalCoordinatorBridgePresenter.delegate = self;
    [self.roomCreationModalCoordinatorBridgePresenter presentFrom:self animated:YES];
}

- (void)showMemberDetails:(MXRoomMember *)member
{
    if (!member)
    {
        return;
    }
    RoomMemberDetailsViewController *memberViewController = [RoomMemberDetailsViewController roomMemberDetailsViewController];
    
    // Set delegate to handle action on member (start chat, mention)
    memberViewController.delegate = self;
    memberViewController.enableMention = (self.inputToolbarView != nil);
    memberViewController.enableVoipCall = NO;
    
    [memberViewController displayRoomMember:member withMatrixRoom:self.roomDataSource.room];
    
    [self.navigationController pushViewController:memberViewController animated:YES];
}

- (void)showRoomAvatarChange
{
    [self showRoomInfoWithInitialSection:RoomInfoSectionChangeAvatar animated:YES];
}

- (void)showAddParticipants
{
    self.participantsInvitePresenter = [[RoomParticipantsInviteCoordinatorBridgePresenter alloc] initWithSession:self.roomDataSource.mxSession room:self.roomDataSource.room parentSpaceId:self.parentSpaceId];
    self.participantsInvitePresenter.delegate = self;
    [self.participantsInvitePresenter presentFrom:self animated:YES];
}

- (void)showRoomTopicChange
{
    [self showRoomInfoWithInitialSection:RoomInfoSectionChangeTopic animated:YES];
}

- (void)showRoomInfo
{
    [self showRoomInfoWithInitialSection:RoomInfoSectionNone animated:YES];
}

- (void)showRoomInfoWithInitialSection:(RoomInfoSection)roomInfoSection animated:(BOOL)animated
{
    RoomInfoCoordinatorParameters *parameters = [[RoomInfoCoordinatorParameters alloc] initWithSession:self.roomDataSource.mxSession room:self.roomDataSource.room parentSpaceId:self.parentSpaceId initialSection:roomInfoSection canAddParticipants: !self.isWaitingForOtherParticipants];

    self.roomInfoCoordinatorBridgePresenter = [[RoomInfoCoordinatorBridgePresenter alloc] initWithParameters:parameters];
    
    self.roomInfoCoordinatorBridgePresenter.delegate = self;
    [self.roomInfoCoordinatorBridgePresenter pushFrom:self.navigationController animated:animated];
}

- (void)setupActions {
    
    if (![self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
        return;
    }
    
    RoomInputToolbarView *roomInputView = ((RoomInputToolbarView *) self.inputToolbarView);
    MXWeakify(self);
    NSMutableArray *actionItems = [NSMutableArray new];
    if (RiotSettings.shared.roomScreenAllowMediaLibraryAction)
    {
        [actionItems addObject:[[RoomActionItem alloc] initWithImage:AssetImages.actionMediaLibrary.image andAction:^{
            MXStrongifyAndReturnIfNil(self);
            if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
                ((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO;
            }
            [self showMediaPickerAnimated:YES];
        }]];
    }
    if (RiotSettings.shared.roomScreenAllowStickerAction && !self.isNewDirectChat)
    {
        [actionItems addObject:[[RoomActionItem alloc] initWithImage:AssetImages.actionSticker.image andAction:^{
            MXStrongifyAndReturnIfNil(self);
            if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
                ((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO;
            }
            [self roomInputToolbarViewPresentStickerPicker];
        }]];
    }
    if (RiotSettings.shared.roomScreenAllowFilesAction)
    {
        [actionItems addObject:[[RoomActionItem alloc] initWithImage:AssetImages.actionFile.image andAction:^{
            MXStrongifyAndReturnIfNil(self);
            if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
                ((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO;
            }
            [self roomInputToolbarViewDidTapFileUpload];
        }]];
    }
    if (RiotSettings.shared.enableVoiceBroadcast && !self.isNewDirectChat)
    {
        [actionItems addObject:[[RoomActionItem alloc] initWithImage:AssetImages.actionLive.image andAction:^{
            MXStrongifyAndReturnIfNil(self);
            if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
                ((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO;
            }
            [self roomInputToolbarViewDidTapVoiceBroadcast];
        }]];
    }
    if (BuildSettings.pollsEnabled && self.displayConfiguration.sendingPollsEnabled && !self.isNewDirectChat)
    {
        [actionItems addObject:[[RoomActionItem alloc] initWithImage:AssetImages.actionPoll.image andAction:^{
            MXStrongifyAndReturnIfNil(self);
            if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
                ((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO;
            }
            [self.delegate roomViewControllerDidRequestPollCreationFormPresentation:self];
        }]];
    }
    if (BuildSettings.locationSharingEnabled && !self.isNewDirectChat)
    {
        [actionItems addObject:[[RoomActionItem alloc] initWithImage:AssetImages.actionLocation.image andAction:^{
            MXStrongifyAndReturnIfNil(self);
            if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
                ((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO;
            }
            [self.delegate roomViewControllerDidRequestLocationSharingFormPresentation:self];
        }]];
    }
    if (RiotSettings.shared.roomScreenAllowCameraAction)
    {
        [actionItems addObject:[[RoomActionItem alloc] initWithImage:AssetImages.actionCamera.image andAction:^{
            MXStrongifyAndReturnIfNil(self);
            if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
                ((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO;
            }
            [self showCameraControllerAnimated:YES];
        }]];
    }
    roomInputView.actionsBar.actionItems = actionItems;
}

- (NSString *)textInputContextIdentifier
{
    return self.roomDataSource.roomId;
}

- (void)roomInputToolbarViewPresentStickerPicker
{
    // Search for the sticker picker widget in the user account
    Widget *widget = [[WidgetManager sharedManager] userWidgets:self.roomDataSource.mxSession ofTypes:@[kWidgetTypeStickerPicker]].firstObject;
    
    if (widget)
    {
        // Display the widget
        [widget widgetUrl:^(NSString * _Nonnull widgetUrl) {
            
            StickerPickerViewController *stickerPickerVC = [[StickerPickerViewController alloc] initWithUrl:widgetUrl forWidget:widget];
            
            stickerPickerVC.roomDataSource = self.roomDataSource;
            
            [self.navigationController pushViewController:stickerPickerVC animated:YES];
        } failure:^(NSError * _Nonnull error) {
            
            MXLogDebug(@"[RoomVC] Cannot display widget %@", widget);
            [self showError:error];
        }];
    }
    else
    {
        // The Sticker picker widget is not installed yet. Propose the user to install it
        MXWeakify(self);
        
        [currentAlert dismissViewControllerAnimated:NO completion:nil];
        
        NSString *alertMessage = [NSString stringWithFormat:@"%@\n%@",
                                  [VectorL10n widgetStickerPickerNoStickerpacksAlert],
                                  [VectorL10n widgetStickerPickerNoStickerpacksAlertAddNow]];
                                   
        UIAlertController *installPrompt = [UIAlertController alertControllerWithTitle:nil
                                                                               message:alertMessage
                                                                        preferredStyle:UIAlertControllerStyleAlert];
        
        [installPrompt addAction:[UIAlertAction actionWithTitle:[VectorL10n no]
                                                          style:UIAlertActionStyleCancel
                                                        handler:^(UIAlertAction * action)
                                 {
            MXStrongifyAndReturnIfNil(self);
            self->currentAlert = nil;
            
        }]];
        
        [installPrompt addAction:[UIAlertAction actionWithTitle:[VectorL10n yes]
                                                          style:UIAlertActionStyleDefault
                                                        handler:^(UIAlertAction * action)
                                 {
            MXStrongifyAndReturnIfNil(self);
            self->currentAlert = nil;
            
            // Show the sticker picker settings screen
            IntegrationManagerViewController *modularVC = [[IntegrationManagerViewController alloc]
                                                           initForMXSession:self.roomDataSource.mxSession
                                                           inRoom:self.roomDataSource.roomId
                                                           screen:[IntegrationManagerViewController screenForWidget:kWidgetTypeStickerPicker]
                                                           widgetId:nil];
            
            [self presentViewController:modularVC animated:NO completion:nil];
        }]];
        
        [installPrompt mxk_setAccessibilityIdentifier:@"RoomVCStickerPickerAlert"];
        [self presentViewController:installPrompt animated:YES completion:nil];
        currentAlert = installPrompt;
    }
}

- (void)roomInputToolbarViewDidTapFileUpload
{
    MXKDocumentPickerPresenter *documentPickerPresenter = [MXKDocumentPickerPresenter new];
    documentPickerPresenter.delegate = self;
    
    NSArray<MXKUTI*> *allowedUTIs = @[MXKUTI.data];
    [documentPickerPresenter presentDocumentPickerWith:allowedUTIs from:self animated:YES completion:nil];
    
    self.documentPickerPresenter = documentPickerPresenter;
}

- (void)roomInputToolbarViewDidTapVoiceBroadcast
{
    // Check first the room permission
    if (![self canSendStateEventWithType:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType])
    {
        [self showAlertWithTitle:[VectorL10n voiceBroadcastUnauthorizedTitle] message:[VectorL10n voiceBroadcastPermissionDeniedMessage]];
        return;
    }
    
    MXSession* session = self.roomDataSource.mxSession;
    // Check whether the user is not already broadcasting here or in another room
    if (session.voiceBroadcastService)
    {
        [self showAlertWithTitle:[VectorL10n voiceBroadcastUnauthorizedTitle] message:[VectorL10n voiceBroadcastAlreadyInProgressMessage]];
        return;
    }
    
    // Prevents listening a VB when recording a new one
    [VoiceBroadcastPlaybackProvider.shared pausePlaying];
    
    // Check connectivity
    if ([AppDelegate theDelegate].isOffline)
    {
        [self showAlertWithTitle:[VectorL10n voiceBroadcastConnectionErrorTitle] message:[VectorL10n voiceBroadcastConnectionErrorMessage]];
        return;
    }
    
    // Request the voice broadcast service to start recording - No service is returned if someone else is already broadcasting in the room
    [session getOrCreateVoiceBroadcastServiceFor:self.roomDataSource.room completion:^(VoiceBroadcastService *voiceBroadcastService) {
        if (voiceBroadcastService) {
            [voiceBroadcastService startVoiceBroadcastWithSuccess:^(NSString * _Nullable success) { } failure:^(NSError * _Nonnull error) {
                [self showAlertWithTitle:[VectorL10n voiceBroadcastConnectionErrorTitle] message:[VectorL10n voiceBroadcastConnectionErrorMessage]];
                [session tearDownVoiceBroadcastService];
            }];
        }
        else
        {
            [self showAlertWithTitle:[VectorL10n voiceBroadcastUnauthorizedTitle] message:[VectorL10n voiceBroadcastBlockedBySomeoneElseMessage]];
        }
    }];
}

/**
 Send a video asset via the room input toolbar prompting the user for the conversion preset to use
 if the `showMediaCompressionPrompt` setting has been enabled.
 @param videoAsset The video asset to send
 @param isPhotoLibraryAsset Whether the asset was picked from the user's photo library.
 */
- (void)sendVideoAsset:(AVAsset *)videoAsset isPhotoLibraryAsset:(BOOL)isPhotoLibraryAsset
{
    if (![self inputToolbarConformsToToolbarViewProtocol])
    {
        return;
    }
    
    if (RiotSettings.shared.showMediaCompressionPrompt)
    {
        // Show the video conversion prompt for the user to select what size video they would like to send.
        UIAlertController *compressionPrompt = [MXKTools videoConversionPromptForVideoAsset:videoAsset
                                                                              withCompletion:^(NSString *presetName) {
            // When the preset name is missing, the user cancelled.
            if (!presetName)
            {
                return;
            }
            
            // Set the chosen preset and send the video (conversion takes place in the SDK).
            [MXSDKOptions sharedInstance].videoConversionPresetName = presetName;
            
            // Create before sending the message in case of a discussion (direct chat)
            [self createDiscussionIfNeeded:^(BOOL readyToSend) {
                if (readyToSend && [self inputToolbarConformsToToolbarViewProtocol])
                {
                    [self.inputToolbarView sendSelectedVideoAsset:videoAsset isPhotoLibraryAsset:isPhotoLibraryAsset];
                }
                // Errors are handled at the request level. This should be improved in case of code rewriting.
            }];
        }];
        
        UIView *sourceView;
        
        if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class])
        {
            sourceView = ((RoomInputToolbarView*)self.inputToolbarView).attachMediaButton;
        }
        else
        {
            sourceView = self.inputToolbarView;
        }
        
        compressionPrompt.popoverPresentationController.sourceView = sourceView;
        compressionPrompt.popoverPresentationController.sourceRect = sourceView.bounds;
        
        [self presentViewController:compressionPrompt animated:YES completion:nil];
    }
    else
    {
        // Otherwise default to 1080p and send the video.
        [MXSDKOptions sharedInstance].videoConversionPresetName = AVAssetExportPreset1920x1080;
        
        // Create before sending the message in case of a discussion (direct chat)
        [self createDiscussionIfNeeded:^(BOOL readyToSend) {
            if (readyToSend && [self inputToolbarConformsToToolbarViewProtocol])
            {
                [self.inputToolbarView sendSelectedVideoAsset:videoAsset isPhotoLibraryAsset:isPhotoLibraryAsset];
            }
            // Errors are handled at the request level. This should be improved in case of code rewriting.
        }];
    }
}

- (void)showRoomWithId:(NSString*)roomId
{
    if (self.delegate)
    {
        [self.delegate roomViewController:self showRoomWithId:roomId eventId:nil];
    }
    else
    {
        [[AppDelegate theDelegate] showRoom:roomId andEventId:nil withMatrixSession:self.roomDataSource.mxSession];
    }
}

- (void)notifyDelegateOnLeaveRoomIfNecessary {
    if (isRoomLeft) {
        return;
    }
    isRoomLeft = YES;
    
    if (self.delegate)
    {
        [self.delegate roomViewControllerDidLeaveRoom:self];
    }
    else
    {
        [[AppDelegate theDelegate] restoreInitialDisplay:^{}];
    }
}

- (void)roomPreviewDidTapCancelAction
{
    // Decline this invitation = leave this page
    if (self.delegate)
    {
        [self.delegate roomViewControllerPreviewDidTapCancel:self];
    }
    else
    {
        [[AppDelegate theDelegate] restoreInitialDisplay:^{}];
    }
}

- (void)startChatWithUserId:(NSString *)userId completion:(void (^)(void))completion
{
    if (self.delegate)
    {
        [self.delegate roomViewController:self startChatWithUserId:userId completion:completion];
    }
    else
    {
        [[AppDelegate theDelegate] showNewDirectChat:userId withMatrixSession:self.mainSession completion:completion];
    }
}

- (void)showError:(NSError*)error
{
    [[AppDelegate theDelegate] showErrorAsAlert:error];
}

- (UIAlertController*)showAlertWithTitle:(NSString*)title message:(NSString*)message
{
    return [[AppDelegate theDelegate] showAlertWithTitle:title message:message];
}

- (ScreenPresentationParameters*)buildUniversalLinkPresentationParameters
{
    return [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:BuildSettings.allowSplitViewDetailsScreenStacking sender:self sourceView:nil];
}

- (BOOL)handleUniversalLinkURL:(NSURL*)url
{
    ScreenPresentationParameters *screenParameters = [self buildUniversalLinkPresentationParameters];
    UniversalLinkParameters *parameters = [[UniversalLinkParameters alloc] initWithUrl:url
                                                                presentationParameters:screenParameters];
    return [self handleUniversalLinkWithParameters:parameters];
}

- (BOOL)handleUniversalLinkFragment:(NSString*)fragment fromURL:(NSURL*)url
{
    ScreenPresentationParameters *screenParameters = [self buildUniversalLinkPresentationParameters];
    UniversalLink *universalLink = [[UniversalLink alloc] initWithUrl:url];
    UniversalLinkParameters *parameters = [[UniversalLinkParameters alloc] initWithFragment:fragment
                                                                              universalLink:universalLink
                                                                     presentationParameters:screenParameters];
    return [self handleUniversalLinkWithParameters:parameters];
}

- (BOOL)handleUniversalLinkWithParameters:(UniversalLinkParameters*)parameters
{
    Analytics.shared.joinedRoomTrigger = AnalyticsJoinedRoomTriggerTimeline;
    
    if (self.delegate)
    {
        return [self.delegate roomViewController:self handleUniversalLinkWithParameters:parameters];
    }
    else
    {
        return [[AppDelegate theDelegate] handleUniversalLinkWithParameters:parameters];
    }
}

- (void)setupCompletionSuggestionViewIfNeeded
{
    if(!self.isViewLoaded) {
        return;
    }
    
    UIViewController *suggestionsViewController = self.completionSuggestionCoordinator.toPresentable;
    
    if (!suggestionsViewController)
    {
        return;
    }
    
    [suggestionsViewController.view setTranslatesAutoresizingMaskIntoConstraints:NO];
    
    [self addChildViewController:suggestionsViewController];
    [self.completionSuggestionContainerView addSubview:suggestionsViewController.view];
    
    [NSLayoutConstraint activateConstraints:@[[suggestionsViewController.view.topAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.topAnchor],
                                              [suggestionsViewController.view.leadingAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.leadingAnchor],
                                              [suggestionsViewController.view.trailingAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.trailingAnchor],
                                              [suggestionsViewController.view.bottomAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.bottomAnchor],]];
    
    [suggestionsViewController didMoveToParentViewController:self];
}

- (void)updateTopBanners
{
    [self.view bringSubviewToFront:self.topBannersStackView];
    
    [self updateLiveLocationBannerViewVisibility];
}

- (void)showEmojiPickerForEventId:(NSString *)eventId
{
    EmojiPickerCoordinatorBridgePresenter *emojiPickerCoordinatorBridgePresenter = [[EmojiPickerCoordinatorBridgePresenter alloc] initWithSession:self.mainSession roomId:self.roomDataSource.roomId eventId:eventId];
    emojiPickerCoordinatorBridgePresenter.delegate = self;
    
    NSInteger cellRow = [self.roomDataSource indexOfCellDataWithEventId:eventId];
    
    UIView *sourceView;
    CGRect sourceRect = CGRectNull;
    
    if (cellRow >= 0)
    {
        NSIndexPath *cellIndexPath = [NSIndexPath indexPathForRow:cellRow inSection:0];
        UITableViewCell *cell = [self.bubblesTableView cellForRowAtIndexPath:cellIndexPath];
        sourceView = cell;
        
        if ([cell isKindOfClass:[MXKRoomBubbleTableViewCell class]])
        {
            MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell*)cell;
            NSInteger bubbleComponentIndex = [roomBubbleTableViewCell.bubbleData bubbleComponentIndexForEventId:eventId];
            sourceRect = [roomBubbleTableViewCell componentFrameInContentViewForIndex:bubbleComponentIndex];
        }
        
    }
    
    [emojiPickerCoordinatorBridgePresenter presentFrom:self sourceView:sourceView sourceRect:sourceRect animated:YES];
    self.emojiPickerCoordinatorBridgePresenter = emojiPickerCoordinatorBridgePresenter;
}

#pragma mark - Jitsi

- (void)showJitsiCallWithWidget:(Widget*)widget
{
    [[AppDelegate theDelegate].callPresenter displayJitsiCallWithWidget:widget];
}

- (void)endActiveJitsiCall
{
    [[AppDelegate theDelegate].callPresenter endActiveJitsiCall];
}

- (BOOL)isRoomHavingAJitsiCall
{
    return [self isRoomHavingAJitsiCallForWidgetId:self.roomDataSource.roomId];
}

- (BOOL)isRoomHavingAJitsiCallForWidgetId:(NSString*)widgetId
{
    return [[AppDelegate theDelegate].callPresenter.jitsiVC.widget.roomId isEqualToString:widgetId];
}

#pragma mark - Dialpad

- (void)openDialpad
{
    DialpadViewController *controller = [DialpadViewController instantiateWithConfiguration:[DialpadConfiguration default]];
    controller.delegate = self;
    self.customSizedPresentationController = [[CustomSizedPresentationController alloc] initWithPresentedViewController:controller presentingViewController:self];
    self.customSizedPresentationController.dismissOnBackgroundTap = NO;
    self.customSizedPresentationController.cornerRadius = 16;
    
    controller.transitioningDelegate = self.customSizedPresentationController;
    [self presentViewController:controller animated:YES completion:nil];
}

#pragma mark - DialpadViewControllerDelegate

- (void)dialpadViewControllerDidTapCall:(DialpadViewController *)viewController withPhoneNumber:(NSString *)phoneNumber
{
    if (self.mainSession.callManager && phoneNumber.length > 0)
    {
        [self startActivityIndicator];
        
        [viewController dismissViewControllerAnimated:YES completion:^{
            MXWeakify(self);
            [self.mainSession.callManager placeCallAgainst:phoneNumber withVideo:NO success:^(MXCall * _Nonnull call) {
                MXStrongifyAndReturnIfNil(self);
                [self stopActivityIndicator];
                self.customSizedPresentationController = nil;
                
                //  do nothing extra here. UI will be handled automatically by the CallService.
            } failure:^(NSError * _Nullable error) {
                MXStrongifyAndReturnIfNil(self);
                [self stopActivityIndicator];
            }];
        }];
    }
}

- (void)dialpadViewControllerDidTapClose:(DialpadViewController *)viewController
{
    [viewController dismissViewControllerAnimated:YES completion:nil];
    self.customSizedPresentationController = nil;
}

#pragma mark - Hide/Show preview header

- (void)showPreviewHeader:(BOOL)isVisible
{
    if (self.previewHeaderContainer && self.previewHeaderContainer.isHidden == isVisible)
    {
        // Check conditions before making the preview room header visible.
        // This operation is ignored if a screen rotation is in progress,
        // or if the view controller is not embedded inside a split view controller yet.
        if (isVisible && (isSizeTransitionInProgress == YES || !self.splitViewController))
        {
            MXLogDebug(@"[RoomVC] Show preview header ignored");
            return;
        }
        
        if (isVisible)
        {
            PreviewRoomTitleView *previewHeader = [PreviewRoomTitleView roomTitleView];
            previewHeader.delegate = self;
            previewHeader.tapGestureDelegate = self;
            previewHeader.translatesAutoresizingMaskIntoConstraints = NO;
            [self.previewHeaderContainer addSubview:previewHeader];
            
            self->previewHeader = previewHeader;
            
            // Force preview header in full width
            NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:previewHeader
                                                                              attribute:NSLayoutAttributeLeading
                                                                              relatedBy:NSLayoutRelationEqual
                                                                                 toItem:self.previewHeaderContainer
                                                                              attribute:NSLayoutAttributeLeading
                                                                             multiplier:1.0
                                                                               constant:0];
            NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:previewHeader
                                                                               attribute:NSLayoutAttributeTrailing
                                                                               relatedBy:NSLayoutRelationEqual
                                                                                  toItem:self.previewHeaderContainer
                                                                               attribute:NSLayoutAttributeTrailing
                                                                              multiplier:1.0
                                                                                constant:0];
            // Vertical constraints are required for iOS > 8
            NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:previewHeader
                                                                             attribute:NSLayoutAttributeTop
                                                                             relatedBy:NSLayoutRelationEqual
                                                                                toItem:self.previewHeaderContainer
                                                                             attribute:NSLayoutAttributeTop
                                                                            multiplier:1.0
                                                                              constant:0];
            NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint constraintWithItem:previewHeader
                                                                                attribute:NSLayoutAttributeBottom
                                                                                relatedBy:NSLayoutRelationEqual
                                                                                   toItem:self.previewHeaderContainer
                                                                                attribute:NSLayoutAttributeBottom
                                                                               multiplier:1.0
                                                                                 constant:0];
            
            [NSLayoutConstraint activateConstraints:@[leftConstraint, rightConstraint, topConstraint, bottomConstraint]];
            
            if (roomPreviewData)
            {
                previewHeader.roomPreviewData = roomPreviewData;
            }
            else if (self.roomDataSource)
            {
                previewHeader.mxRoom = self.roomDataSource.room;
            }
            
            self.previewHeaderContainer.hidden = NO;
            
            // Finalize preview header display according to the screen orientation
            [self refreshPreviewHeader:UIInterfaceOrientationIsLandscape([[UIApplication sharedApplication] statusBarOrientation])];
        }
        else
        {
            [previewHeader removeFromSuperview];
            previewHeader = nil;
            
            self.previewHeaderContainer.hidden = YES;
            
            // Consider the main navigation controller if the current view controller is embedded inside a split view controller.
            UINavigationController *mainNavigationController = self.navigationController;
            if (self.splitViewController.isCollapsed && self.splitViewController.viewControllers.count)
            {
                mainNavigationController = self.splitViewController.viewControllers.firstObject;
            }
            
            // Set a default title view class without handling tap gesture (Let [self refreshRoomTitle] refresh this view correctly).
            [self setRoomTitleViewClass:RoomTitleView.class];
                        
            // Remove the shadow image used to hide the bottom border of the navigation bar when the preview header is displayed
            [mainNavigationController.navigationBar setShadowImage:nil];
            [mainNavigationController.navigationBar setBackgroundImage:nil forBarMetrics:UIBarMetricsDefault];
            
            [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn
                             animations:^{
                
                self.bubblesTableViewTopConstraint.constant = 0;
                
                // Force to render the view
                [self forceLayoutRefresh];
                
            }
                             completion:^(BOOL finished){
            }];
        }
    }
    
    // Consider the main navigation controller if the current view controller is embedded inside a split view controller.
    UINavigationController *mainNavigationController = self.navigationController;
    if (self.splitViewController.isCollapsed && self.splitViewController.viewControllers.count)
    {
        mainNavigationController = self.splitViewController.viewControllers.firstObject;
    }
    mainNavigationController.navigationBar.translucent = isVisible;
    self.navigationController.navigationBar.translucent = isVisible;
}

- (void)refreshPreviewHeader:(BOOL)isLandscapeOriented
{
    if (previewHeader)
    {
        if (isLandscapeOriented
            && [GBDeviceInfo deviceInfo].family != GBDeviceFamilyiPad)
        {
            CGRect frame = self.navigationController.navigationBar.frame;
            
            previewHeader.mainHeaderContainer.hidden = YES;
            previewHeader.mainHeaderBackgroundHeightConstraint.constant = frame.size.height + (frame.origin.y > 0 ? frame.origin.y : 0);
            
            [self setRoomTitleViewClass:RoomTitleView.class];
            // We don't want to handle tap gesture here
            
            // Remove details icon
            RoomTitleView *roomTitleView = (RoomTitleView*)self.titleView;
            
            // Set preview data to provide the room name
            roomTitleView.roomPreviewData = roomPreviewData;
        }
        else
        {
            previewHeader.mainHeaderContainer.hidden = NO;
            previewHeader.mainHeaderBackgroundHeightConstraint.constant = previewHeader.mainHeaderContainer.frame.size.height;
            
            if ([previewHeader isKindOfClass:PreviewRoomTitleView.class])
            {
                // In case of preview, update the header height so that we can
                // display as much as possible the room topic in this header.
                // Note: the header height is handled by the previewHeader.mainHeaderBackgroundHeightConstraint.
                PreviewRoomTitleView *previewRoomTitleView = (PreviewRoomTitleView *)previewHeader;
                
                // Compute the height required to display all the room topic
                CGSize sizeThatFitsTextView = [previewRoomTitleView.roomTopic sizeThatFits:CGSizeMake(previewRoomTitleView.roomTopic.frame.size.width, MAXFLOAT)];
                
                // Increase the preview header height according to the room topic height
                // but limit it in order to let room for room messages at the screen bottom.
                // This free space depends on the device.
                // On an iphone 5 screen, the room topic height cannot be more than 50px.
                // Then, on larger screen, we can allow it a bit more height but we
                // apply a factor to give more priority to the display of more messages.
                CGFloat screenHeight = [[UIScreen mainScreen] bounds].size.height;
                CGFloat maxRoomTopicHeight = 50 + (screenHeight - 568) / 3;
                
                CGFloat additionalHeight = MIN(maxRoomTopicHeight, sizeThatFitsTextView.height)
                - previewRoomTitleView.roomTopic.frame.size.height;
                
                previewHeader.mainHeaderBackgroundHeightConstraint.constant += additionalHeight;
            }
            
            [self setRoomTitleViewClass:RoomAvatarTitleView.class];
            // Note the avatar title view does not define tap gesture.
            
            previewHeader.roomAvatar.alpha = 0.0;
            
            // Set the avatar provided in preview data
            if (roomPreviewData.roomAvatarUrl)
            {
                previewHeader.roomAvatarURL = roomPreviewData.roomAvatarUrl;
            }
            else if (roomPreviewData.roomId && roomPreviewData.roomName)
            {
                previewHeader.roomAvatarPlaceholder = [AvatarGenerator generateAvatarForMatrixItem:roomPreviewData.roomId withDisplayName:roomPreviewData.roomName];
            }
            else
            {
                previewHeader.roomAvatarPlaceholder = [MXKTools paintImage:AssetImages.placeholder.image
                                                                 withColor:ThemeService.shared.theme.tintColor];
            }
        }
        
        // Force the layout of previewHeader to update the position of 'bottomBorderView' which is used
        // to define the actual height of the preview container.
        [previewHeader layoutIfNeeded];
        CGRect frame = previewHeader.bottomBorderView.frame;
        self.previewHeaderContainerHeightConstraint.constant = frame.origin.y + frame.size.height;
        
        // Consider the main navigation controller if the current view controller is embedded inside a split view controller.
        UINavigationController *mainNavigationController = self.navigationController;
        if (self.splitViewController.isCollapsed && self.splitViewController.viewControllers.count)
        {
            mainNavigationController = self.splitViewController.viewControllers.firstObject;
        }
        
        // When the preview header is displayed, we hide the bottom border of the navigation bar (the shadow image).
        // The default shadow image is nil. When non-nil, this property represents a custom shadow image to show instead
        // of the default. For a custom shadow image to be shown, a custom background image must also be set with the
        // setBackgroundImage:forBarMetrics: method. If the default background image is used, then the default shadow
        // image will be used regardless of the value of this property.
        UIImage *shadowImage = [[UIImage alloc] init];
        [mainNavigationController.navigationBar setShadowImage:shadowImage];
        [mainNavigationController.navigationBar setBackgroundImage:shadowImage forBarMetrics:UIBarMetricsDefault];
        
        [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn
                         animations:^{
            
            self.bubblesTableViewTopConstraint.constant = self.previewHeaderContainerHeightConstraint.constant - self.bubblesTableView.adjustedContentInset.top;
            
            self->previewHeader.roomAvatar.alpha = 1;
            
            // Force to render the view
            [self forceLayoutRefresh];
            
        }
                         completion:^(BOOL finished){
        }];
    }
}

#pragma mark - Preview

- (void)displayRoomPreview:(RoomPreviewData *)previewData
{
    // Release existing room data source or preview
    [self displayRoom:nil];
    
    if (previewData)
    {
        self.eventsAcknowledgementEnabled = NO;
        
        [self addMatrixSession:previewData.mxSession];
        
        roomPreviewData = previewData;
        
        [self refreshRoomTitle];
        
        if (roomPreviewData.roomDataSource)
        {
            [super displayRoom:roomPreviewData.roomDataSource];
        }
    }
}

#pragma mark - New discussion

- (void)displayNewDirectChatWithTargetUser:(nonnull MXUser*)directChatTargetUser session:(nonnull MXSession*)session
{
    // `[displayRoom:]` may require the session, setting it here before calling it
    [self addMatrixSession:session];

    // Release existing room data source or preview
    [self displayRoom:nil];
    
    self.directChatTargetUser = directChatTargetUser;
    
    self.eventsAcknowledgementEnabled = NO;

    [self refreshRoomTitle];
    [self refreshRoomInputToolbar];
}

#pragma mark - MXKDataSourceDelegate

- (Class<MXKCellRendering>)cellViewClassForCellData:(MXKCellData*)cellData
{
    RoomTimelineCellIdentifier cellIdentifier = [self cellIdentifierForCellData:cellData andRoomDataSource:self.customizedRoomDataSource];
    
    RoomTimelineConfiguration *timelineConfiguration = [RoomTimelineConfiguration shared];
            
    return [timelineConfiguration.currentStyle.cellProvider cellViewClassForCellIdentifier:cellIdentifier];;
}

- (RoomTimelineCellIdentifier)cellIdentifierForCellData:(MXKCellData*)cellData andRoomDataSource:(RoomDataSource *)customizedRoomDataSource;
{
    // Sanity check
    if (![cellData conformsToProtocol:@protocol(MXKRoomBubbleCellDataStoring)])
    {
        return RoomTimelineCellIdentifierUnknown;
    }
    
    BOOL showEncryptionBadge = NO;
    RoomTimelineCellIdentifier cellIdentifier;
        
    id<MXKRoomBubbleCellDataStoring> bubbleData = (id<MXKRoomBubbleCellDataStoring>)cellData;
    
    MXKRoomBubbleCellData *roomBubbleCellData;
    
    if ([bubbleData isKindOfClass:MXKRoomBubbleCellData.class])
    {
        roomBubbleCellData = (MXKRoomBubbleCellData*)bubbleData;
        showEncryptionBadge = roomBubbleCellData.containsBubbleComponentWithEncryptionBadge;
    }
    
    // Select the suitable table view cell class, by considering first the empty bubble cell.
    if (bubbleData.hasNoDisplay)
    {
        cellIdentifier = RoomTimelineCellIdentifierEmpty;
    }
    else if (bubbleData.tag == RoomBubbleCellDataTagRoomCreationIntro)
    {
        cellIdentifier = RoomTimelineCellIdentifierRoomCreationIntro;
    }
    else if (bubbleData.tag == RoomBubbleCellDataTagRoomCreateWithPredecessor)
    {
        cellIdentifier = RoomTimelineCellIdentifierRoomPredecessor;
    }
    else if (bubbleData.tag == RoomBubbleCellDataTagKeyVerificationRequestIncomingApproval)
    {
        cellIdentifier = bubbleData.isPaginationFirstBubble ? RoomTimelineCellIdentifierKeyVerificationIncomingRequestApprovalWithPaginationTitle : RoomTimelineCellIdentifierKeyVerificationIncomingRequestApproval;
    }
    else if (bubbleData.tag == RoomBubbleCellDataTagKeyVerificationRequest)
    {
        cellIdentifier = bubbleData.isPaginationFirstBubble ? RoomTimelineCellIdentifierKeyVerificationRequestStatusWithPaginationTitle : RoomTimelineCellIdentifierKeyVerificationRequestStatus;
    }
    else if (bubbleData.tag == RoomBubbleCellDataTagKeyVerificationConclusion)
    {
        cellIdentifier = bubbleData.isPaginationFirstBubble ? RoomTimelineCellIdentifierKeyVerificationConclusionWithPaginationTitle : RoomTimelineCellIdentifierKeyVerificationConclusion;
    }
    else if (bubbleData.tag == RoomBubbleCellDataTagMembership)
    {
        if (bubbleData.collapsed)
        {
            if (bubbleData.nextCollapsableCellData)
            {
                cellIdentifier = bubbleData.isPaginationFirstBubble ? RoomTimelineCellIdentifierMembershipCollapsedWithPaginationTitle : RoomTimelineCellIdentifierMembershipCollapsed;
            }
            else
            {
                // Use a normal membership cell for a single membership event
                cellIdentifier = bubbleData.isPaginationFirstBubble ? RoomTimelineCellIdentifierMembershipWithPaginationTitle : RoomTimelineCellIdentifierMembership;
            }
        }
        else if (bubbleData.collapsedAttributedTextMessage)
        {
            // The cell (and its series) is not collapsed but this cell is the first
            // of the series. So, use the cell with the "collapse" button.
            cellIdentifier = bubbleData.isPaginationFirstBubble ? RoomTimelineCellIdentifierMembershipExpandedWithPaginationTitle : RoomTimelineCellIdentifierMembershipExpanded;
        }
        else
        {
            cellIdentifier = bubbleData.isPaginationFirstBubble ? RoomTimelineCellIdentifierMembershipWithPaginationTitle : RoomTimelineCellIdentifierMembership;
        }
    }
    else if (bubbleData.tag == RoomBubbleCellDataTagRoomCreateConfiguration)
    {
        cellIdentifier = bubbleData.isPaginationFirstBubble ? RoomTimelineCellIdentifierRoomCreationCollapsedWithPaginationTitle : RoomTimelineCellIdentifierRoomCreationCollapsed;
    }
    else if (bubbleData.tag == RoomBubbleCellDataTagCall)
    {
        cellIdentifier = RoomTimelineCellIdentifierDirectCallStatus;
    }
    else if (bubbleData.tag == RoomBubbleCellDataTagGroupCall)
    {
        cellIdentifier = RoomTimelineCellIdentifierGroupCallStatus;
    }
    else if (bubbleData.tag == RoomBubbleCellDataTagRTCCallNotify)
    {
        cellIdentifier = RoomTimelineCellIdentifierMatrixRTCCall;
    }
    else if (bubbleData.attachment.type == MXKAttachmentTypeVoiceMessage || bubbleData.attachment.type == MXKAttachmentTypeAudio)
    {
        if (bubbleData.isIncoming)
        {
            if (bubbleData.isPaginationFirstBubble)
            {
                cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceMessageWithPaginationTitle;
            }
            else if (bubbleData.shouldHideSenderInformation)
            {
                cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceMessageWithoutSenderInfo;
            }
            else
            {
                cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceMessage;
            }
        }
        else
        {
            if (bubbleData.isPaginationFirstBubble)
            {
                cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceMessageWithPaginationTitle;
            }
            else if (bubbleData.shouldHideSenderInformation)
            {
                cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceMessageWithoutSenderInfo;
            }
            else
            {
                cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceMessage;
            }
        }
    }
    else if (bubbleData.tag == RoomBubbleCellDataTagPoll)
    {
        if (bubbleData.isIncoming)
        {
            if (bubbleData.isPaginationFirstBubble)
            {
                cellIdentifier = RoomTimelineCellIdentifierIncomingPollWithPaginationTitle;
            }
            else if (bubbleData.shouldHideSenderInformation)
            {
                cellIdentifier = RoomTimelineCellIdentifierIncomingPollWithoutSenderInfo;
            }
            else
            {
                cellIdentifier = RoomTimelineCellIdentifierIncomingPoll;
            }
        }
        else
        {
            if (bubbleData.isPaginationFirstBubble)
            {
                cellIdentifier = RoomTimelineCellIdentifierOutgoingPollWithPaginationTitle;
            }
            else if (bubbleData.shouldHideSenderInformation)
            {
                cellIdentifier = RoomTimelineCellIdentifierOutgoingPollWithoutSenderInfo;
            }
            else
            {
                cellIdentifier = RoomTimelineCellIdentifierOutgoingPoll;
            }
        }
    }
    else if (bubbleData.tag == RoomBubbleCellDataTagLocation || bubbleData.tag == RoomBubbleCellDataTagLiveLocation)
    {
        if (bubbleData.isIncoming)
        {
            if (bubbleData.isPaginationFirstBubble)
            {
                cellIdentifier = RoomTimelineCellIdentifierIncomingLocationWithPaginationTitle;
            }
            else if (bubbleData.shouldHideSenderInformation)
            {
                cellIdentifier = RoomTimelineCellIdentifierIncomingLocationWithoutSenderInfo;
            }
            else
            {
                cellIdentifier = RoomTimelineCellIdentifierIncomingLocation;
            }
        }
        else
        {
            if (bubbleData.isPaginationFirstBubble)
            {
                cellIdentifier = RoomTimelineCellIdentifierOutgoingLocationWithPaginationTitle;
            }
            else if (bubbleData.shouldHideSenderInformation)
            {
                cellIdentifier = RoomTimelineCellIdentifierOutgoingLocationWithoutSenderInfo;
            }
            else
            {
                cellIdentifier = RoomTimelineCellIdentifierOutgoingLocation;
            }
        }
    }
    else if (bubbleData.tag == RoomBubbleCellDataTagVoiceBroadcastPlayback)
    {
        if (bubbleData.isIncoming)
        {
            if (bubbleData.isPaginationFirstBubble)
            {
                cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceBroadcastPlaybackWithPaginationTitle;
            }
            else if (bubbleData.shouldHideSenderInformation)
            {
                cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceBroadcastPlaybackWithoutSenderInfo;
            }
            else
            {
                cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceBroadcastPlayback;
            }
        }
        else
        {
            if (bubbleData.isPaginationFirstBubble)
            {
                cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlaybackWithPaginationTitle;
            }
            else if (bubbleData.shouldHideSenderInformation)
            {
                cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlaybackWithoutSenderInfo;
            }
            else
            {
                cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlayback;
            }
        }
    }
    else if (bubbleData.tag == RoomBubbleCellDataTagVoiceBroadcastRecord)
    {
        if (bubbleData.isPaginationFirstBubble)
        {
            cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorderWithPaginationTitle;
        }
        else if (bubbleData.shouldHideSenderInformation)
        {
            cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorderWithoutSenderInfo;
        }
        else
        {
            cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorder;
        }
    }
    
    else if (roomBubbleCellData.getFirstBubbleComponentWithDisplay.event.isEmote)
    {
        if (bubbleData.isIncoming)
        {
            if (bubbleData.isPaginationFirstBubble)
            {
                if (bubbleData.shouldHideSenderName)
                {
                    cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingEmoteEncryptedWithPaginationTitleWithoutSenderName : RoomTimelineCellIdentifierIncomingEmoteWithPaginationTitleWithoutSenderName;
                }
                else
                {
                    cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingEmoteEncryptedWithPaginationTitle : RoomTimelineCellIdentifierIncomingEmoteWithPaginationTitle;
                }
            }
            else if (bubbleData.shouldHideSenderInformation)
            {
                cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingEmoteEncryptedWithoutSenderInfo : RoomTimelineCellIdentifierIncomingEmoteWithoutSenderInfo;
            }
            else if (bubbleData.shouldHideSenderName)
            {
                cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingEmoteEncryptedWithoutSenderName : RoomTimelineCellIdentifierIncomingEmoteWithoutSenderName;
            }
            else
            {
                cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingEmoteEncrypted : RoomTimelineCellIdentifierIncomingEmote;
            }
        }
        else
        {
            if (bubbleData.isPaginationFirstBubble)
            {
                if (bubbleData.shouldHideSenderName)
                {
                    cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingEmoteEncryptedWithPaginationTitleWithoutSenderName : RoomTimelineCellIdentifierOutgoingEmoteWithPaginationTitleWithoutSenderName;
                }
                else
                {
                    cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingEmoteEncryptedWithPaginationTitle : RoomTimelineCellIdentifierOutgoingEmoteWithPaginationTitle;
                }
            }
            else if (bubbleData.shouldHideSenderInformation)
            {
                cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingEmoteEncryptedWithoutSenderInfo : RoomTimelineCellIdentifierOutgoingEmoteWithoutSenderInfo;
            }
            else if (bubbleData.shouldHideSenderName)
            {
                cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingEmoteEncryptedWithoutSenderName : RoomTimelineCellIdentifierOutgoingEmoteWithoutSenderName;
            }
            else
            {
                cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingEmoteEncrypted : RoomTimelineCellIdentifierOutgoingEmote;
            }
        }
    }
    else if (bubbleData.isIncoming)
    {
        if (bubbleData.isAttachmentWithThumbnail)
        {
            // Check whether the provided celldata corresponds to a selected sticker
            if (customizedRoomDataSource.selectedEventId && (bubbleData.attachment.type == MXKAttachmentTypeSticker) && [bubbleData.attachment.eventId isEqualToString:customizedRoomDataSource.selectedEventId])
            {
                cellIdentifier = RoomTimelineCellIdentifierSelectedSticker;
            }
            else if (bubbleData.isPaginationFirstBubble)
            {
                cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingAttachmentEncryptedWithPaginationTitle : RoomTimelineCellIdentifierIncomingAttachmentWithPaginationTitle;
            }
            else if (bubbleData.shouldHideSenderInformation)
            {
                cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingAttachmentEncryptedWithoutSenderInfo : RoomTimelineCellIdentifierIncomingAttachmentWithoutSenderInfo;
            }
            else
            {
                cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingAttachmentEncrypted : RoomTimelineCellIdentifierIncomingAttachment;
            }
        }
        else if (bubbleData.isAttachment)
        {
            if (bubbleData.isPaginationFirstBubble)
            {
                cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailEncryptedWithPaginationTitle : RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailWithPaginationTitle;
            }
            else if (bubbleData.shouldHideSenderInformation)
            {
                cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailEncryptedWithoutSenderInfo : RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailWithoutSenderInfo;
            }
            else
            {
                cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailEncrypted : RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnail;
            }
        }
        else
        {
            if (bubbleData.isPaginationFirstBubble)
            {
                if (bubbleData.shouldHideSenderName)
                {
                    cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingTextMessageEncryptedWithPaginationTitleWithoutSenderName : RoomTimelineCellIdentifierIncomingTextMessageWithPaginationTitleWithoutSenderName;
                }
                else
                {
                    cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingTextMessageEncryptedWithPaginationTitle : RoomTimelineCellIdentifierIncomingTextMessageWithPaginationTitle;
                }
            }
            else if (bubbleData.shouldHideSenderInformation)
            {
                cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingTextMessageEncryptedWithoutSenderInfo : RoomTimelineCellIdentifierIncomingTextMessageWithoutSenderInfo;
            }
            else if (bubbleData.shouldHideSenderName)
            {
                cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingTextMessageEncryptedWithoutSenderName : RoomTimelineCellIdentifierIncomingTextMessageWithoutSenderName;
            }
            else
            {
                cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingTextMessageEncrypted : RoomTimelineCellIdentifierIncomingTextMessage;
            }
        }
    }
    else
    {
        // Handle here outgoing bubbles
        if (bubbleData.isAttachmentWithThumbnail)
        {
            // Check whether the provided celldata corresponds to a selected sticker
            if (customizedRoomDataSource.selectedEventId && (bubbleData.attachment.type == MXKAttachmentTypeSticker) && [bubbleData.attachment.eventId isEqualToString:customizedRoomDataSource.selectedEventId])
            {
                cellIdentifier = RoomTimelineCellIdentifierSelectedSticker;
            }
            else if (bubbleData.isPaginationFirstBubble)
            {
                cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingAttachmentEncryptedWithPaginationTitle : RoomTimelineCellIdentifierOutgoingAttachmentWithPaginationTitle;
            }
            else if (bubbleData.shouldHideSenderInformation)
            {
                cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingAttachmentEncryptedWithoutSenderInfo : RoomTimelineCellIdentifierOutgoingAttachmentWithoutSenderInfo;
            }
            else
            {
                cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingAttachmentEncrypted : RoomTimelineCellIdentifierOutgoingAttachment;
            }
        }
        else if (bubbleData.isAttachment)
        {
            if (bubbleData.isPaginationFirstBubble)
            {
                cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailEncryptedWithPaginationTitle : RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailWithPaginationTitle;
            }
            else if (bubbleData.shouldHideSenderInformation)
            {
                cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailEncryptedWithoutSenderInfo : RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailWithoutSenderInfo;
            }
            else
            {
                cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailEncrypted : RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnail;
            }
        }
        else
        {
            if (bubbleData.isPaginationFirstBubble)
            {
                if (bubbleData.shouldHideSenderName)
                {
                    cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingTextMessageEncryptedWithPaginationTitleWithoutSenderName : RoomTimelineCellIdentifierOutgoingTextMessageWithPaginationTitleWithoutSenderName;
                }
                else
                {
                    cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingTextMessageEncryptedWithPaginationTitle : RoomTimelineCellIdentifierOutgoingTextMessageWithPaginationTitle;
                }
            }
            else if (bubbleData.shouldHideSenderInformation)
            {
                cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingTextMessageEncryptedWithoutSenderInfo : RoomTimelineCellIdentifierOutgoingTextMessageWithoutSenderInfo;
            }
            else if (bubbleData.shouldHideSenderName)
            {
                cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingTextMessageEncryptedWithoutSenderName : RoomTimelineCellIdentifierOutgoingTextMessageWithoutSenderName;
            }
            else
            {
                cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingTextMessageEncrypted : RoomTimelineCellIdentifierOutgoingTextMessage;
            }
        }
    }
    
    return cellIdentifier;
}

#pragma mark - MXKDataSource delegate

- (void)dataSource:(MXKDataSource *)dataSource didRecognizeAction:(NSString *)actionIdentifier inCell:(id<MXKCellRendering>)cell userInfo:(NSDictionary *)userInfo
{
    // Handle here user actions on bubbles for Vector app
    if (self.customizedRoomDataSource)
    {
        id<MXKRoomBubbleCellDataStoring> bubbleData;
        
        if ([cell isKindOfClass:[MXKRoomBubbleTableViewCell class]])
        {
            MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell*)cell;
            bubbleData = roomBubbleTableViewCell.bubbleData;
        }
        
        
        if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnAvatarView])
        {
            MXRoomMember *member = [self.roomDataSource.roomState.members memberWithUserId:userInfo[kMXKRoomBubbleCellUserIdKey]];
            [self showMemberDetails:member];
        }
        else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellLongPressOnAvatarView])
        {
            // Add the member display name in text input
            MXRoomMember *roomMember = [self.roomDataSource.roomState.members memberWithUserId:userInfo[kMXKRoomBubbleCellUserIdKey]];
            if (roomMember)
            {
                [self mention:roomMember];
            }
        }
        else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellStopShareButtonPressed])
        {
            NSString *beaconInfoEventId;
            
            if ([bubbleData isKindOfClass:[RoomBubbleCellData class]])
            {
                RoomBubbleCellData *roomBubbleCellData = (RoomBubbleCellData*)bubbleData;
                beaconInfoEventId = roomBubbleCellData.beaconInfoSummary.id;
            }
            
            [self.delegate roomViewControllerDidStopLiveLocationSharing:self beaconInfoEventId:beaconInfoEventId];
        }
        else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellRetryShareButtonPressed])
        {
            MXEvent *selectedEvent = userInfo[kMXKRoomBubbleCellEventKey];
            if (selectedEvent)
            {
                // TODO: - Implement retry live location action
            }
        }
        else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnMessageTextView] || [actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnContentView])
        {
            // Retrieve the tapped event
            MXEvent *tappedEvent = userInfo[kMXKRoomBubbleCellEventKey];
            
            // Check whether a selection already exist or not
            if (self.customizedRoomDataSource.selectedEventId)
            {
                [self cancelEventSelection];
            }
            else if (bubbleData.tag == RoomBubbleCellDataTagLiveLocation)
            {
                [self.delegate roomViewController:self didRequestLiveLocationPresentationForBubbleData:bubbleData];
            }
            else if (tappedEvent)
            {
                if (tappedEvent.eventType == MXEventTypeRoomCreate)
                {
                    // Handle tap on RoomPredecessorBubbleCell
                    MXRoomCreateContent *createContent = [MXRoomCreateContent modelFromJSON:tappedEvent.content];
                    NSString *predecessorRoomId = createContent.roomPredecessorInfo.roomId;
                    
                    if (predecessorRoomId)
                    {
                        // Show predecessor room
                        Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerTombstone;
                        [self showRoomWithId:predecessorRoomId];
                    }
                    else
                    {
                        // Show contextual menu on single tap if bubble is not collapsed
                        if (bubbleData.collapsed)
                        {
                            // Do nothing here as we display room creation modal only if the user taps on the room name
                        }
                        else
                        {
                            [self showContextualMenuForEvent:tappedEvent fromSingleTapGesture:YES cell:cell animated:YES];
                        }
                    }
                }
                else if (bubbleData.tag == RoomBubbleCellDataTagCall)
                {
                    if ([bubbleData isKindOfClass:[RoomBubbleCellData class]])
                    {
                        //  post notification `RoomCallTileTapped`
                        [[NSNotificationCenter defaultCenter] postNotificationName:RoomCallTileTappedNotification object:bubbleData];
                        
                        preventBubblesTableViewScroll = YES;
                        [self selectEventWithId:tappedEvent.eventId];
                    }
                }
                else if (bubbleData.tag == RoomBubbleCellDataTagGroupCall)
                {
                    if ([bubbleData isKindOfClass:[RoomBubbleCellData class]])
                    {
                        //  post notification `RoomGroupCallTileTapped`
                        [[NSNotificationCenter defaultCenter] postNotificationName:RoomGroupCallTileTappedNotification object:bubbleData];
                        
                        preventBubblesTableViewScroll = YES;
                        [self selectEventWithId:tappedEvent.eventId];
                    }
                }
                else
                {
                    // Show contextual menu on single tap if bubble is not collapsed
                    if (bubbleData.collapsed)
                    {
                        [self selectEventWithId:tappedEvent.eventId];
                    }
                    else
                    {
                        if (tappedEvent.location) {
                            [_delegate roomViewController:self didRequestLocationPresentationForEvent:tappedEvent bubbleData:bubbleData];
                        } else {
                            [self showContextualMenuForEvent:tappedEvent fromSingleTapGesture:YES cell:cell animated:YES];
                        }
                    }
                }
            }
        }
        else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnOverlayContainer])
        {
            // Cancel the current event selection
            [self cancelEventSelection];
        }
        else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellRiotEditButtonPressed])
        {
            [self dismissKeyboard];
            
            MXEvent *selectedEvent = userInfo[kMXKRoomBubbleCellEventKey];
            
            if (selectedEvent)
            {
                [self showContextualMenuForEvent:selectedEvent fromSingleTapGesture:YES cell:cell animated:YES];
            }
        }
        else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellKeyVerificationIncomingRequestAcceptPressed])
        {
            NSString *eventId = userInfo[kMXKRoomBubbleCellEventIdKey];
            
            RoomDataSource *roomDataSource = (RoomDataSource*)self.roomDataSource;
            
            [roomDataSource acceptVerificationRequestForEventId:eventId success:^{
                
            } failure:^(NSError *error) {
                [self showError:error];
            }];
        }
        else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed])
        {
            NSString *eventId = userInfo[kMXKRoomBubbleCellEventIdKey];
            
            RoomDataSource *roomDataSource = (RoomDataSource*)self.roomDataSource;
            
            [roomDataSource declineVerificationRequestForEventId:eventId success:^{
                
            } failure:^(NSError *error) {
                [self showError:error];
            }];
        }
        else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnAttachmentView])
        {
            if (((MXKRoomBubbleTableViewCell*)cell).bubbleData.attachment.eventSentState == MXEventSentStateFailed)
            {
                // Shortcut: when clicking on an unsent media, show the action sheet to resend it
                NSString *eventId = ((MXKRoomBubbleTableViewCell*)cell).bubbleData.attachment.eventId;
                MXEvent *selectedEvent = [self.roomDataSource eventWithEventId:eventId];
                
                if (selectedEvent)
                {
                    [self dataSource:dataSource didRecognizeAction:kMXKRoomBubbleCellRiotEditButtonPressed inCell:cell userInfo:@{kMXKRoomBubbleCellEventKey:selectedEvent}];
                }
                else
                {
                    MXLogDebug(@"[RoomViewController] didRecognizeAction:inCell:userInfo tap on attachment with event state MXEventSentStateFailed. Selected event is nil for event id %@", eventId);
                }
            }
            else if (((MXKRoomBubbleTableViewCell*)cell).bubbleData.attachment.type == MXKAttachmentTypeSticker)
            {
                // We don't open the attachments viewer when the user taps on a sticker.
                // We consider this tap like a selection.
                
                // Check whether a selection already exist or not
                if (self.customizedRoomDataSource.selectedEventId)
                {
                    [self cancelEventSelection];
                }
                else
                {
                    // Highlight this event in displayed message
                    [self selectEventWithId:((MXKRoomBubbleTableViewCell*)cell).bubbleData.attachment.eventId];
                }
            }
            else
            {
                // Keep default implementation
                [super dataSource:dataSource didRecognizeAction:actionIdentifier inCell:cell userInfo:userInfo];
            }
        }
        else if ([actionIdentifier isEqualToString:kRoomEncryptedDataBubbleCellTapOnEncryptionIcon])
        {
            // Retrieve the tapped event
            MXEvent *tappedEvent = userInfo[kMXKRoomBubbleCellEventKey];
            
            if (tappedEvent)
            {
                [self showEncryptionInformation:tappedEvent];
            }
        }
        else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnReceiptsContainer])
        {
            MXKReceiptSendersContainer *container = userInfo[kMXKRoomBubbleCellReceiptsContainerKey];
            [ReadReceiptsViewController openInViewController:self fromContainer:container withSession:self.mainSession];
        }
        else if ([actionIdentifier isEqualToString:kRoomMembershipExpandedBubbleCellTapOnCollapseButton])
        {
            // Reset the selection before collapsing
            self.customizedRoomDataSource.selectedEventId = nil;
            
            [self.roomDataSource collapseRoomBubble:((MXKRoomBubbleTableViewCell*)cell).bubbleData collapsed:YES];
        }
        else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellLongPressOnEvent])
        {
            MXEvent *tappedEvent = userInfo[kMXKRoomBubbleCellEventKey];
            
            if (!bubbleData.collapsed)
            {
                [self handleLongPressFromCell:cell withTappedEvent:tappedEvent];
            }
        }
        else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellLongPressOnReactionView])
        {
            NSString *tappedEventId = userInfo[kMXKRoomBubbleCellEventIdKey];
            if (tappedEventId)
            {
                [self showReactionHistoryForEventId:tappedEventId animated:YES];
            }
        }
        else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnAddReaction])
        {
            NSString *tappedEventId = userInfo[kMXKRoomBubbleCellEventIdKey];
            if (tappedEventId)
            {
                [self showEmojiPickerForEventId:tappedEventId];
            }
        }
        else if ([actionIdentifier isEqualToString:RoomDirectCallStatusCell.callBackAction])
        {
            MXEvent *callInviteEvent = userInfo[kMXKRoomBubbleCellEventKey];
            MXCallInviteEventContent *eventContent = [MXCallInviteEventContent modelFromJSON:callInviteEvent.content];
            
            [self placeCallWithVideo2:eventContent.isVideoCall];
        }
        else if ([actionIdentifier isEqualToString:RoomDirectCallStatusCell.declineAction])
        {
            MXEvent *callInviteEvent = userInfo[kMXKRoomBubbleCellEventKey];
            MXCallInviteEventContent *eventContent = [MXCallInviteEventContent modelFromJSON:callInviteEvent.content];
            
            MXCall *call = [self.mainSession.callManager callWithCallId:eventContent.callId];
            [call hangup];
        }
        else if ([actionIdentifier isEqualToString:RoomDirectCallStatusCell.answerAction])
        {
            MXEvent *callInviteEvent = userInfo[kMXKRoomBubbleCellEventKey];
            MXCallInviteEventContent *eventContent = [MXCallInviteEventContent modelFromJSON:callInviteEvent.content];
            
            MXCall *call = [self.mainSession.callManager callWithCallId:eventContent.callId];
            [call answer];
        }
        else if ([actionIdentifier isEqualToString:RoomDirectCallStatusCell.endCallAction])
        {
            MXEvent *callInviteEvent = userInfo[kMXKRoomBubbleCellEventKey];
            MXCallInviteEventContent *eventContent = [MXCallInviteEventContent modelFromJSON:callInviteEvent.content];
            
            MXCall *call = [self.mainSession.callManager callWithCallId:eventContent.callId];
            [call hangup];
        }
        else if ([actionIdentifier isEqualToString:RoomGroupCallStatusCell.joinAction] ||
                 [actionIdentifier isEqualToString:RoomGroupCallStatusCell.answerAction])
        {
            MXWeakify(self);

            // Check app permissions first
            [MXKTools checkAccessForCall:YES
             manualChangeMessageForAudio:[VectorL10n microphoneAccessNotGrantedForCall:AppInfo.current.displayName]
             manualChangeMessageForVideo:[VectorL10n cameraAccessNotGrantedForCall:AppInfo.current.displayName]
               showPopUpInViewController:self completionHandler:^(BOOL granted) {
                
                MXStrongifyAndReturnIfNil(self);
                if (granted)
                {
                    // Present the Jitsi view controller
                    Widget *jitsiWidget = [self.customizedRoomDataSource jitsiWidget];
                    if (jitsiWidget)
                    {
                        [self showJitsiCallWithWidget:jitsiWidget];
                    }
                }
                else
                {
                    MXLogDebug(@"[RoomVC] didRecognizeAction:inCell:userInfo Warning: The application does not have the permission to join/answer the group call");
                }
            }];
            
            MXEvent *widgetEvent = userInfo[kMXKRoomBubbleCellEventKey];
            Widget *widget = [[Widget alloc] initWithWidgetEvent:widgetEvent
                                                 inMatrixSession:self.customizedRoomDataSource.mxSession];
            [[JitsiService shared] resetDeclineForWidgetWithId:widget.widgetId];
        }
        else if ([actionIdentifier isEqualToString:RoomGroupCallStatusCell.leaveAction])
        {
            [self endActiveJitsiCall];
            [self reloadBubblesTable:YES];
        }
        else if ([actionIdentifier isEqualToString:RoomGroupCallStatusCell.declineAction])
        {
            MXEvent *widgetEvent = userInfo[kMXKRoomBubbleCellEventKey];
            Widget *widget = [[Widget alloc] initWithWidgetEvent:widgetEvent
                                                 inMatrixSession:self.customizedRoomDataSource.mxSession];
            [[JitsiService shared] declineWidgetWithId:widget.widgetId];
            [self reloadBubblesTable:YES];
        }
        else if ([actionIdentifier isEqualToString:RoomCreationIntroCell.tapOnAvatarView])
        {
            [self showRoomAvatarChange];
        }
        else if ([actionIdentifier isEqualToString:RoomCreationIntroCell.tapOnAddParticipants])
        {
            [self showAddParticipants];
        }
        else if ([actionIdentifier isEqualToString:RoomCreationIntroCell.tapOnAddTopic])
        {
            [self showRoomTopicChange];
        }
        else if ([actionIdentifier isEqualToString:RoomCreationIntroCell.tapOnRoomName])
        {
            [self showRoomCreationModal];
        }
        else
        {
            // Keep default implementation for other actions
            [super dataSource:dataSource didRecognizeAction:actionIdentifier inCell:cell userInfo:userInfo];
        }
    }
    else
    {
        // Keep default implementation for other actions
        [super dataSource:dataSource didRecognizeAction:actionIdentifier inCell:cell userInfo:userInfo];
    }
}

// Display the additiontal event actions menu
- (void)showAdditionalActionsMenuForEvent:(MXEvent*)selectedEvent inCell:(id<MXKCellRendering>)cell animated:(BOOL)animated
{
    MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell;
    MXKAttachment *attachment = roomBubbleTableViewCell.bubbleData.attachment;
    
    BOOL isJitsiCallEvent = NO;
    switch (selectedEvent.eventType) {
        case MXEventTypeCustom:
            if ([selectedEvent.type isEqualToString:kWidgetMatrixEventTypeString]
                || [selectedEvent.type isEqualToString:kWidgetModularEventTypeString])
            {
                Widget *widget = [[Widget alloc] initWithWidgetEvent:selectedEvent inMatrixSession:self.roomDataSource.mxSession];
                if ([widget.type isEqualToString:kWidgetTypeJitsiV1] ||
                    [widget.type isEqualToString:kWidgetTypeJitsiV2])
                {
                    isJitsiCallEvent = YES;
                }
            }
        default:
            break;
    }
    
    if (currentAlert)
    {
        [currentAlert dismissViewControllerAnimated:NO completion:nil];
        currentAlert = nil;
    }
    
    [self.eventMenuBuilder reset];
    
    MXWeakify(self);
    
    BOOL showThreadOption = [self showThreadOptionForEvent:selectedEvent];
    if (showThreadOption && [self canCopyEvent:selectedEvent andCell:cell])
    {
        MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell;
        MXKRoomBubbleCellData *cellData = roomBubbleTableViewCell.bubbleData;
        [self.eventMenuBuilder addItemWithType:EventMenuItemTypeCopy
                                        action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCopy]
                                                                        style:UIAlertActionStyleDefault
                                                                      handler:^(UIAlertAction * action) {
            MXStrongifyAndReturnIfNil(self);
            
            [self cancelEventSelection];
            
            [self copyEvent:selectedEvent inCell:cell withCellData:cellData];
        }]];
    }
    
    // Add actions for a failed event
    if (selectedEvent.sentState == MXEventSentStateFailed)
    {
        [self.eventMenuBuilder addItemWithType:EventMenuItemTypeRetrySending
                                        action:[UIAlertAction actionWithTitle:[VectorL10n retry]
                                                                        style:UIAlertActionStyleDefault
                                                                      handler:^(UIAlertAction * action) {
            MXStrongifyAndReturnIfNil(self);
            
            [self cancelEventSelection];
            
            // Let the datasource resend. It will manage local echo, etc.
            [self.roomDataSource resendEventWithEventId:selectedEvent.eventId success:nil failure:nil];
        }]];
        
        [self.eventMenuBuilder addItemWithType:EventMenuItemTypeRemove
                                        action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionDelete]
                                                                        style:UIAlertActionStyleDestructive
                                                                      handler:^(UIAlertAction * action) {
            MXStrongifyAndReturnIfNil(self);
            
            [self cancelEventSelection];
            
            [self.roomDataSource removeEventWithEventId:selectedEvent.eventId];
        }]];
    }
    
    // View in room action
    if (self.roomDataSource.threadId && [selectedEvent.eventId isEqualToString:self.roomDataSource.threadId])
    {
        //  if in the thread and selected event is the root event
        //  add "View in room" action
        [self.eventMenuBuilder addItemWithType:EventMenuItemTypeViewInRoom
                                        action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewInRoom]
                                                                        style:UIAlertActionStyleDefault
                                                                      handler:^(UIAlertAction * action) {
            MXStrongifyAndReturnIfNil(self);
            [self.delegate roomViewController:self
                               showRoomWithId:self.roomDataSource.roomId
                                      eventId:selectedEvent.eventId];
        }]];
    }
    
    // Add actions for text message
    if (!attachment)
    {
        // Retrieved data related to the selected event
        NSArray *components = roomBubbleTableViewCell.bubbleData.bubbleComponents;
        MXKRoomBubbleComponent *selectedComponent;
        for (selectedComponent in components)
        {
            if ([selectedComponent.event.eventId isEqualToString:selectedEvent.eventId])
            {
                break;
            }
            selectedComponent = nil;
        }
        
        
        // Check status of the selected event
        if (selectedEvent.sentState == MXEventSentStatePreparing ||
            selectedEvent.sentState == MXEventSentStateEncrypting ||
            selectedEvent.sentState == MXEventSentStateSending)
        {
            [self.eventMenuBuilder addItemWithType:EventMenuItemTypeCancelSending
                                            action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCancelSend]
                                                                            style:UIAlertActionStyleDefault
                                                                          handler:^(UIAlertAction * action) {
                MXStrongifyAndReturnIfNil(self);
                
                self->currentAlert = nil;
                
                // Cancel and remove the outgoing message
                [self.roomDataSource.room cancelSendingOperation:selectedEvent.eventId];
                [self.roomDataSource removeEventWithEventId:selectedEvent.eventId];
                
                [self cancelEventSelection];
            }]];
        }
        
        if (selectedEvent.sentState == MXEventSentStateSent &&
            !selectedEvent.isTimelinePollEvent &&
            // Forwarding of live-location shares still to be implemented
            selectedEvent.eventType != MXEventTypeBeaconInfo)
        {
            [self.eventMenuBuilder addItemWithType:EventMenuItemTypeForward
                                            action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward]
                                                                            style:UIAlertActionStyleDefault
                                                                          handler:^(UIAlertAction * action) {
                MXStrongifyAndReturnIfNil(self);

                [self cancelEventSelection];

                [self presentEventForwardingDialogForSelectedEvent:selectedEvent];
            }]];
        }
        
        if (!isJitsiCallEvent && BuildSettings.messageDetailsAllowShare && !selectedEvent.isTimelinePollEvent &&
            selectedEvent.eventType != MXEventTypeBeaconInfo)
        {
            [self.eventMenuBuilder addItemWithType:EventMenuItemTypeShare
                                            action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionShare]
                                                                            style:UIAlertActionStyleDefault
                                                                          handler:^(UIAlertAction * action) {
                MXStrongifyAndReturnIfNil(self);
                
                [self cancelEventSelection];
                
                UIActivityViewController *activityViewController = nil;
                if (selectedEvent.location) {
                    activityViewController = [self.delegate roomViewController:self locationShareActivityViewControllerForEvent:selectedEvent];
                }
                
                if (activityViewController == nil && selectedComponent.textMessage) {
                    NSArray *activityItems = @[selectedComponent.textMessage];
                    activityViewController = [[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:nil];
                }
                
                if (activityViewController)
                {
                    activityViewController.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
                    activityViewController.popoverPresentationController.sourceView = roomBubbleTableViewCell;
                    activityViewController.popoverPresentationController.sourceRect = roomBubbleTableViewCell.bounds;
                    
                    [self presentViewController:activityViewController animated:YES completion:nil];
                }
            }]];
        }
    }
    else // Add action for attachment
    {
        // Forwarding for already sent attachments
        if (selectedEvent.sentState == MXEventSentStateSent && (attachment.type == MXKAttachmentTypeFile ||
                                                                attachment.type == MXKAttachmentTypeImage ||
                                                                attachment.type == MXKAttachmentTypeVideo ||
                                                                attachment.type == MXKAttachmentTypeVoiceMessage)) {
            [self.eventMenuBuilder addItemWithType:EventMenuItemTypeForward
                                            action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward]
                                                                            style:UIAlertActionStyleDefault
                                                                          handler:^(UIAlertAction * action) {
                MXStrongifyAndReturnIfNil(self);

                [self cancelEventSelection];
                
                [self presentEventForwardingDialogForSelectedEvent:selectedEvent];
            }]];
        }
        
        if (BuildSettings.messageDetailsAllowSave)
        {
            if (attachment.type == MXKAttachmentTypeImage || attachment.type == MXKAttachmentTypeVideo)
            {
                [self.eventMenuBuilder addItemWithType:EventMenuItemTypeSaveMedia
                                                action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionSave]
                                                                                style:UIAlertActionStyleDefault
                                                                              handler:^(UIAlertAction * action) {
                    MXStrongifyAndReturnIfNil(self);
                    
                    [self cancelEventSelection];
                    
                    [self startActivityIndicator];
                    
                    MXWeakify(self);
                    [attachment save:^{
                        MXStrongifyAndReturnIfNil(self);
                        [self stopActivityIndicator];
                    } failure:^(NSError *error) {
                        MXStrongifyAndReturnIfNil(self);
                        [self stopActivityIndicator];
                        
                        //Alert user
                        [self showError:error];
                    }];
                    
                    // Start animation in case of download during attachment preparing
                    [roomBubbleTableViewCell startProgressUI];
                }]];
            }
        }
        
        // Check status of the selected event
        if (selectedEvent.sentState == MXEventSentStatePreparing ||
            selectedEvent.sentState == MXEventSentStateEncrypting ||
            selectedEvent.sentState == MXEventSentStateUploading ||
            selectedEvent.sentState == MXEventSentStateSending)
        {
            // Upload id is stored in attachment url (nasty trick)
            NSString *uploadId = roomBubbleTableViewCell.bubbleData.attachment.contentURL;
            if ([MXMediaManager existingUploaderWithId:uploadId])
            {
                [self.eventMenuBuilder addItemWithType:EventMenuItemTypeCancelSending
                                                action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCancelSend]
                                                                                style:UIAlertActionStyleDefault
                                                                              handler:^(UIAlertAction * action) {
                    
                    MXStrongifyAndReturnIfNil(self);
                    
                    // Get again the loader
                    MXMediaLoader *loader = [MXMediaManager existingUploaderWithId:uploadId];
                    if (loader)
                    {
                        [loader cancel];
                    }
                    // Hide the progress animation
                    roomBubbleTableViewCell.progressView.hidden = YES;
                    
                    self->currentAlert = nil;
                    
                    // Remove the outgoing message and its related cached file.
                    [[NSFileManager defaultManager] removeItemAtPath:roomBubbleTableViewCell.bubbleData.attachment.cacheFilePath error:nil];
                    [[NSFileManager defaultManager] removeItemAtPath:roomBubbleTableViewCell.bubbleData.attachment.thumbnailCachePath error:nil];
                    
                    // Cancel and remove the outgoing message
                    [self.roomDataSource.room cancelSendingOperation:selectedEvent.eventId];
                    [self.roomDataSource removeEventWithEventId:selectedEvent.eventId];
                    
                    [self cancelEventSelection];
                }]];
            }
        }
        
        if (attachment.type != MXKAttachmentTypeSticker)
        {
            if (BuildSettings.messageDetailsAllowShare)
            {
                [self.eventMenuBuilder addItemWithType:EventMenuItemTypeShare
                                                action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionShare]
                                                                                style:UIAlertActionStyleDefault
                                                                              handler:^(UIAlertAction * action) {
                    MXStrongifyAndReturnIfNil(self);
                    
                    [self cancelEventSelection];
                    
                    [self startActivityIndicator];
                    
                    MXWeakify(self);
                    [attachment prepareShare:^(NSURL *fileURL) {
                        MXStrongifyAndReturnIfNil(self);
                        
                        [self stopActivityIndicator];
                        
                        self->documentInteractionController = [UIDocumentInteractionController interactionControllerWithURL:fileURL];
                        [self->documentInteractionController setDelegate:self];
                        self->currentSharedAttachment = attachment;
                        
                        if (![self->documentInteractionController presentOptionsMenuFromRect:self.view.frame inView:self.view animated:YES])
                        {
                            self->documentInteractionController = nil;
                            [attachment onShareEnded];
                            self->currentSharedAttachment = nil;
                        }
                        
                    } failure:^(NSError *error) {
                        [self showError:error];
                        [self stopActivityIndicator];
                    }];
                    
                    // Start animation in case of download during attachment preparing
                    [roomBubbleTableViewCell startProgressUI];
                }]];
            }
        }
    }
    
    // Check status of the selected event
    if (selectedEvent.sentState == MXEventSentStateSent)
    {
        // Check whether download is in progress
        if (selectedEvent.isMediaAttachment)
        {
            NSString *downloadId = roomBubbleTableViewCell.bubbleData.attachment.downloadId;
            if ([MXMediaManager existingDownloaderWithIdentifier:downloadId])
            {
                [self.eventMenuBuilder addItemWithType:EventMenuItemTypeCancelDownloading
                                                action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCancelDownload]
                                                                                style:UIAlertActionStyleDefault
                                                                              handler:^(UIAlertAction * action) {
                    MXStrongifyAndReturnIfNil(self);
                    
                    [self cancelEventSelection];
                    
                    // Get again the loader
                    MXMediaLoader *loader = [MXMediaManager existingDownloaderWithIdentifier:downloadId];
                    if (loader)
                    {
                        [loader cancel];
                    }
                    // Hide the progress animation
                    roomBubbleTableViewCell.progressView.hidden = YES;
                }]];
            }
        }
        
        if (BuildSettings.messageDetailsAllowPermalink)
        {
            [self.eventMenuBuilder addItemWithType:EventMenuItemTypePermalink
                                            action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionPermalink]
                                                                            style:UIAlertActionStyleDefault
                                                                          handler:^(UIAlertAction * action) {
                MXStrongifyAndReturnIfNil(self);
                
                [self cancelEventSelection];
                
                // Create a matrix.to permalink that is common to all matrix clients
                NSString *permalink = [MXTools permalinkToEvent:selectedEvent.eventId inRoom:selectedEvent.roomId];
                NSURL *url = [NSURL URLWithString:permalink];
                
                if (url)
                {
                    MXKPasteboardManager.shared.pasteboard.URL = url;
                    [self.view vc_toastWithMessage:VectorL10n.roomEventCopyLinkInfo
                                             image:AssetImages.linkIcon.image
                                          duration:2.0
                                          position:ToastPositionBottom
                                  additionalMargin:self.roomInputToolbarContainerHeightConstraint.constant];
                }
                else
                {
                    MXLogDebug(@"[RoomViewController] Contextual menu permalink action failed. Permalink is nil room id/event id: %@/%@", selectedEvent.roomId, selectedEvent.eventId);
                }
            }]];
        }
        
        if (BuildSettings.messageDetailsAllowViewSource)
        {
            [self.eventMenuBuilder addItemWithType:EventMenuItemTypeViewSource
                                            action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewSource]
                                                                            style:UIAlertActionStyleDefault
                                                                          handler:^(UIAlertAction * action) {
                MXStrongifyAndReturnIfNil(self);
                
                [self cancelEventSelection];
                
                // Display event details
                [self showEventDetails:selectedEvent];
            }]];
            
            
            // Add "View Decrypted Source" for e2ee event we can decrypt
            if (selectedEvent.isEncrypted && selectedEvent.clearEvent)
            {
                [self.eventMenuBuilder addItemWithType:EventMenuItemTypeViewDecryptedSource
                                                action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewDecryptedSource]
                                                                                style:UIAlertActionStyleDefault
                                                                              handler:^(UIAlertAction * action) {
                    MXStrongifyAndReturnIfNil(self);
                    
                    [self cancelEventSelection];
                    
                    // Display clear event details
                    [self showEventDetails:selectedEvent.clearEvent];
                }]];
            }
        }
        
        // Do not allow to redact the event that enabled encryption (m.room.encryption)
        // because it breaks everything
        if (selectedEvent.eventType != MXEventTypeRoomEncryption)
        {
            NSString *title;
            EventMenuItemType itemType;
            if (selectedEvent.eventType == MXEventTypePollStart)
            {
                title = [VectorL10n roomEventActionRemovePoll];
                itemType = EventMenuItemTypeRemovePoll;
            }
            else
            {
                title = [VectorL10n roomEventActionRedact];
                itemType = EventMenuItemTypeRemove;
            }
            
            [self.eventMenuBuilder addItemWithType:itemType
                                            action:[UIAlertAction actionWithTitle:title
                                                                            style:UIAlertActionStyleDestructive
                                                                          handler:^(UIAlertAction * action) {
                MXStrongifyAndReturnIfNil(self);
                
                [self cancelEventSelection];
                
                [self startActivityIndicator];
                
                NSArray<NSString *>* relationTypes = nil;
                // If it's a voice broadcast, delete the selected event and all related events.
                if (selectedEvent.eventType == MXEventTypeCustom && [selectedEvent.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]) {
                    relationTypes = @[MXEventRelationTypeReference];
                }
                
                MXWeakify(self);
                [self.roomDataSource.room redactEvent:selectedEvent.eventId withRelations:relationTypes reason:nil success:^{
                    MXStrongifyAndReturnIfNil(self);
                    [self stopActivityIndicator];
                } failure:^(NSError *error) {
                    MXStrongifyAndReturnIfNil(self);
                    [self stopActivityIndicator];
                    
                    MXLogDebug(@"[RoomVC] Redact event (%@) failed", selectedEvent.eventId);
                    //Alert user
                    [self showError:error];
                }];
            }]];
        }
        
        if (selectedEvent.eventType == MXEventTypePollStart && [selectedEvent.sender isEqualToString:self.mainSession.myUserId])
        {
            if ([self.delegate roomViewController:self canEndPollWithEventIdentifier:selectedEvent.eventId])
            {
                [self.eventMenuBuilder addItemWithType:EventMenuItemTypeEndPoll
                                                action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionEndPoll]
                                                                                style:UIAlertActionStyleDefault
                                                                              handler:^(UIAlertAction * action) {
                    MXStrongifyAndReturnIfNil(self);
                    
                    [self.delegate roomViewController:self endPollWithEventIdentifier:selectedEvent.eventId];
                    
                    [self hideContextualMenuAnimated:YES];
                }]];
            }
        }
        
        // Add reaction history if event contains reactions
        if (roomBubbleTableViewCell.bubbleData.reactions[selectedEvent.eventId].aggregatedReactionsWithNonZeroCount)
        {
            [self.eventMenuBuilder addItemWithType:EventMenuItemTypeReactionHistory
                                            action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionReactionHistory]
                                                                            style:UIAlertActionStyleDefault
                                                                          handler:^(UIAlertAction * action) {
                MXStrongifyAndReturnIfNil(self);
                
                [self cancelEventSelection];
                
                // Show reaction history
                [self showReactionHistoryForEventId:selectedEvent.eventId animated:YES];
            }]];
        }
        
        if (![selectedEvent.sender isEqualToString:self.mainSession.myUserId] && RiotSettings.shared.roomContextualMenuShowReportContentOption)
        {
            [self.eventMenuBuilder addItemWithType:EventMenuItemTypeReport
                                            action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionReport]
                                                                            style:UIAlertActionStyleDestructive
                                                                          handler:^(UIAlertAction * action) {
                MXStrongifyAndReturnIfNil(self);
                
                [self cancelEventSelection];
                
                // Prompt user to enter a description of the problem content.
                UIAlertController *reportReasonAlert = [UIAlertController alertControllerWithTitle:[VectorL10n roomEventActionReportPromptReason]
                                                                                           message:nil
                                                                                    preferredStyle:UIAlertControllerStyleAlert];
                
                [reportReasonAlert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
                    textField.secureTextEntry = NO;
                    textField.placeholder = nil;
                    textField.keyboardType = UIKeyboardTypeDefault;
                }];
                
                MXWeakify(self);
                [reportReasonAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
                    MXStrongifyAndReturnIfNil(self);
                    
                    NSString *text = [self->currentAlert textFields].firstObject.text;
                    self->currentAlert = nil;
                    
                    [self startActivityIndicator];
                    
                    MXWeakify(self);
                    [self.roomDataSource.room reportEvent:selectedEvent.eventId score:-100 reason:text success:^{
                        MXStrongifyAndReturnIfNil(self);
                        
                        [self stopActivityIndicator];
                        
                        // Prompt user to ignore content from this user
                        UIAlertController *ignoreUserAlert = [UIAlertController alertControllerWithTitle:[VectorL10n roomEventActionReportPromptIgnoreUser]
                                                                                                 message:nil
                                                                                          preferredStyle:UIAlertControllerStyleAlert];
                        
                        MXWeakify(self);
                        [ignoreUserAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n yes] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
                            
                            MXStrongifyAndReturnIfNil(self);
                            self->currentAlert = nil;
                            
                            [self startActivityIndicator];
                            
                            MXWeakify(self);
                            // Add the user to the blacklist: ignored users
                            [self.mainSession ignoreUsers:@[selectedEvent.sender] success:^{
                                MXStrongifyAndReturnIfNil(self);
                                [self stopActivityIndicator];
                            } failure:^(NSError *error) {
                                MXStrongifyAndReturnIfNil(self);
                                [self stopActivityIndicator];
                                
                                MXLogDebug(@"[RoomVC] Ignore user (%@) failed", selectedEvent.sender);
                                //Alert user
                                [self showError:error];
                            }];
                        }]];
                        
                        [ignoreUserAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n no] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
                            MXStrongifyAndReturnIfNil(self);
                            self->currentAlert = nil;
                        }]];
                        
                        [self presentViewController:ignoreUserAlert animated:YES completion:nil];
                        self->currentAlert = ignoreUserAlert;
                        
                    } failure:^(NSError *error) {
                        MXStrongifyAndReturnIfNil(self);
                        [self stopActivityIndicator];
                        
                        MXLogDebug(@"[RoomVC] Report event (%@) failed", selectedEvent.eventId);
                        //Alert user
                        [self showError:error];
                        
                    }];
                }]];
                
                [reportReasonAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) {
                    MXStrongifyAndReturnIfNil(self);
                    self->currentAlert = nil;
                }]];
                
                [self presentViewController:reportReasonAlert animated:YES completion:nil];
                self->currentAlert = reportReasonAlert;
            }]];
        }
        
        if (!isJitsiCallEvent && self.roomDataSource.room.summary.isEncrypted)
        {
            [self.eventMenuBuilder addItemWithType:EventMenuItemTypeViewEncryption
                                            action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewEncryption]
                                                                            style:UIAlertActionStyleDefault
                                                                          handler:^(UIAlertAction * action) {
                MXStrongifyAndReturnIfNil(self);
                
                [self cancelEventSelection];
                
                // Display encryption details
                [self showEncryptionInformation:selectedEvent];
            }]];
        }
    }

    [self.eventMenuBuilder addItemWithType:EventMenuItemTypeCancel
                                    action:[UIAlertAction actionWithTitle:[VectorL10n cancel]
                                                                    style:UIAlertActionStyleCancel
                                                                  handler:^(UIAlertAction * action) {
        MXStrongifyAndReturnIfNil(self);

        [self hideContextualMenuAnimated:YES];
    }]];
    
    // Do not display empty action sheet
    if (!self.eventMenuBuilder.isEmpty)
    {
        UIAlertController *actionsMenu = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
        
        //  build actions and add them to the alert
        NSArray<UIAlertAction*> *actions = [self.eventMenuBuilder build];
        for (UIAlertAction *action in actions)
        {
            [actionsMenu addAction:action];
        }
        
        NSInteger bubbleComponentIndex = [roomBubbleTableViewCell.bubbleData bubbleComponentIndexForEventId:selectedEvent.eventId];
        
        CGRect sourceRect = [roomBubbleTableViewCell componentFrameInContentViewForIndex:bubbleComponentIndex];
        
        [actionsMenu mxk_setAccessibilityIdentifier:@"RoomVCEventMenuAlert"];
        [actionsMenu popoverPresentationController].sourceView = roomBubbleTableViewCell;
        [actionsMenu popoverPresentationController].sourceRect = sourceRect;
        [self dismissKeyboard];
        [self presentViewController:actionsMenu animated:animated completion:nil];
        currentAlert = actionsMenu;
    }
}

- (void)presentEventForwardingDialogForSelectedEvent:(MXEvent *)selectedEvent
{
    ForwardingShareItemSender *shareItemSender = [[ForwardingShareItemSender alloc] initWithEvent:selectedEvent];
    self.shareManager = [[ShareManager alloc] initWithShareItemSender:shareItemSender
                                                                 type:ShareManagerTypeForward
                                                              session:self.mainSession];
    
    MXWeakify(self);
    [self.shareManager setCompletionCallback:^(ShareManagerResult result) {
        MXStrongifyAndReturnIfNil(self);
        if ([self.presentedViewController isEqual:self.shareManager.mainViewController])
        {
            [self dismissViewControllerAnimated:YES completion:nil];
        }
        self.shareManager = nil;
    }];
    
    [self presentViewController:self.shareManager.mainViewController animated:YES completion:nil];
}

- (BOOL)dataSource:(MXKDataSource *)dataSource shouldDoAction:(NSString *)actionIdentifier inCell:(id<MXKCellRendering>)cell userInfo:(NSDictionary *)userInfo defaultValue:(BOOL)defaultValue
{
    BOOL shouldDoAction = defaultValue;
    
    if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellShouldInteractWithURL])
    {
        // Try to catch universal link supported by the app
        NSURL *url = userInfo[kMXKRoomBubbleCellUrl];
        // Retrieve the type of interaction expected with the URL (See UITextItemInteraction)
        NSNumber *urlItemInteractionValue = userInfo[kMXKRoomBubbleCellUrlItemInteraction];
        
        RoomMessageURLType roomMessageURLType = RoomMessageURLTypeUnknown;
        
        if (url)
        {
            roomMessageURLType = [self.roomMessageURLParser parseURL:url];
        }
        
        // When a link refers to a room alias/id, a user id or an event id, the non-ASCII characters (like '#' in room alias) has been escaped
        // to be able to convert it into a legal URL string.
        NSString *absoluteURLString = [url.absoluteString stringByRemovingPercentEncoding];
        
        // If the link can be open it by the app, let it do
        if ([Tools isUniversalLink:url])
        {
            shouldDoAction = NO;
            
            [self handleUniversalLinkURL:url];
        }
        // Open a detail screen about the clicked user
        else if ([MXTools isMatrixUserIdentifier:absoluteURLString])
        {
            shouldDoAction = NO;
            
            NSString *userId = absoluteURLString;
            
            MXRoomMember* member = [self.roomDataSource.roomState.members memberWithUserId:userId];
            if (member)
            {
                // Use the room member detail VC for room members
                [self showMemberDetails:member];
            }
            else
            {
                // Use the contact detail VC for other users
                MXUser *user = [self.roomDataSource.room.mxSession userWithUserId:userId];
                if (user)
                {
                    selectedContact = [[MXKContact alloc] initMatrixContactWithDisplayName:((user.displayname.length > 0) ? user.displayname : user.userId) andMatrixID:user.userId];
                }
                else
                {
                    selectedContact = [[MXKContact alloc] initMatrixContactWithDisplayName:userId andMatrixID:userId];
                }
                [self performSegueWithIdentifier:@"showContactDetails" sender:self];
            }
        }
        // Open the clicked room
        else if ([MXTools isMatrixRoomIdentifier:absoluteURLString] || [MXTools isMatrixRoomAlias:absoluteURLString])
        {
            shouldDoAction = NO;
            
            NSString *roomIdOrAlias = absoluteURLString;
            
            // Create a permalink to open or preview the room.
            NSString *permalink = [MXTools permalinkToRoom:roomIdOrAlias];
            NSURL *permalinkURL = [NSURL URLWithString:permalink];
            
            [self handleUniversalLinkURL:permalinkURL];
        }
        else if ([absoluteURLString hasPrefix:EventFormatterOnReRequestKeysLinkAction])
        {
            NSArray<NSString*> *arguments = [absoluteURLString componentsSeparatedByString:EventFormatterLinkActionSeparator];
            if (arguments.count > 1)
            {
                NSString *eventId = arguments[1];
                MXEvent *event = [self.roomDataSource eventWithEventId:eventId];
                
                if (event)
                {
                    [self reRequestKeysAndShowExplanationAlert:event];
                }
            }
        }
        else if ([absoluteURLString hasPrefix:EventFormatterEditedEventLinkAction])
        {
            NSArray<NSString*> *arguments = [absoluteURLString componentsSeparatedByString:EventFormatterLinkActionSeparator];
            if (arguments.count > 1)
            {
                NSString *eventId = arguments[1];
                [self showEditHistoryForEventId:eventId animated:YES];
            }
            shouldDoAction = NO;
        }
        else if (url && urlItemInteractionValue)
        {
            // Fallback case for external links
            switch (urlItemInteractionValue.integerValue) {
                case UITextItemInteractionInvokeDefaultAction:
                {
                    switch (roomMessageURLType) {
                        case RoomMessageURLTypeAppleDataDetector:
                            // Keep the default OS behavior on single tap when UITextView data detector detect a known type.
                            shouldDoAction = YES;
                            break;
                        case RoomMessageURLTypeDummy:
                            // Do nothing for dummy links
                            shouldDoAction = NO;
                            break;
                        case RoomMessageURLTypeHttp:
                            shouldDoAction = YES;
                            break;
                        default:
                        {
                            MXEvent *tappedEvent = userInfo[kMXKRoomBubbleCellEventKey];
                            URLValidationResult *result = [URLValidator validateTappedURL:url in:tappedEvent];
                            if (result.shouldShowConfirmationAlert)
                            {
                                [self showDifferentURLsAlertFor:url
                                                  visibleURLString:result.visibleURLString];
                                return NO;
                            }
                            // Try to open the link
                            [[UIApplication sharedApplication] vc_open:url completionHandler:^(BOOL success) {
                                if (!success)
                                {
                                    [self showUnableToOpenLinkErrorAlert];
                                }
                            }];
                            shouldDoAction = NO;
                            break;
                        }
                    }
                }
                    break;
                case UITextItemInteractionPresentActions:
                {
                    if (roomMessageURLType == RoomMessageURLTypeHttp) {
                        shouldDoAction = YES;
                    } else {
                        // Retrieve the tapped event
                        MXEvent *tappedEvent = userInfo[kMXKRoomBubbleCellEventKey];
                        
                        if (tappedEvent)
                        {
                            // Long press on link, present room contextual menu.
                            [self showContextualMenuForEvent:tappedEvent fromSingleTapGesture:NO cell:cell animated:YES];
                        }
                        
                        shouldDoAction = NO;
                    }
                }
                    break;
                case UITextItemInteractionPreview:
                    // Force touch on link, let MXKRoomBubbleTableViewCell UITextView use default peek and pop behavior.
                    break;
                default:
                    break;
            }
        }
        else
        {
            [self showUnableToOpenLinkErrorAlert];
        }
    }
    
    return shouldDoAction;
}

- (void)selectEventWithId:(NSString*)eventId
{
    [self selectEventWithId:eventId inputToolBarSendMode:RoomInputToolbarViewSendModeSend showTimestamp:YES];
}

- (void)selectEventWithId:(NSString*)eventId inputToolBarSendMode:(RoomInputToolbarViewSendMode)inputToolBarSendMode showTimestamp:(BOOL)showTimestamp
{
    [self setInputToolBarSendMode:inputToolBarSendMode forEventWithId:eventId];
    
    self.customizedRoomDataSource.showBubbleDateTimeOnSelection = showTimestamp;
    self.customizedRoomDataSource.selectedEventId = eventId;
    
    // Force table refresh
    [self dataSource:self.roomDataSource didCellChange:nil];
}

- (void)cancelEventSelection
{
    [self setInputToolBarSendMode:RoomInputToolbarViewSendModeSend forEventWithId:nil];
    
    if (currentAlert)
    {
        [currentAlert dismissViewControllerAnimated:NO completion:nil];
        currentAlert = nil;
    }
    
    self.customizedRoomDataSource.showBubbleDateTimeOnSelection = YES;
    self.customizedRoomDataSource.selectedEventId = nil;
    self.customizedRoomDataSource.highlightedEventId = nil;
    
    [self restoreTextMessageBeforeEditing];
    
    // Force table refresh
    [self dataSource:self.roomDataSource didCellChange:nil];
}

- (void)showUnableToOpenLinkErrorAlert
{
    [self showAlertWithTitle:[VectorL10n error]
                     message:[VectorL10n roomMessageUnableOpenLinkErrorMessage]];
}

- (void)editEventContentWithId:(NSString*)eventId
{
    MXEvent *event = [self.roomDataSource eventWithEventId:eventId];
    
    if ([self inputToolbarConformsToHtmlToolbarViewProtocol])
    {
        MXKRoomInputToolbarView <HtmlRoomInputToolbarViewProtocol> *htmlInputToolBarView = (MXKRoomInputToolbarView <HtmlRoomInputToolbarViewProtocol> *) self.inputToolbarView;
        self.htmlTextBeforeEditing = htmlInputToolBarView.htmlContent;
        htmlInputToolBarView.htmlContent = [self.customizedRoomDataSource editableHtmlTextMessageFor:event];
    }
    else if ([self inputToolbarConformsToToolbarViewProtocol])
    {
        self.textMessageBeforeEditing = self.inputToolbarView.attributedTextMessage;
        self.inputToolbarView.attributedTextMessage = [self.customizedRoomDataSource editableAttributedTextMessageFor:event];
    }
    
    [self selectEventWithId:eventId inputToolBarSendMode:RoomInputToolbarViewSendModeEdit showTimestamp:YES];
}

- (void)restoreTextMessageBeforeEditing
{
    
   
    if (self.htmlTextBeforeEditing && [self inputToolbarConformsToHtmlToolbarViewProtocol])
    {
        MXKRoomInputToolbarView <HtmlRoomInputToolbarViewProtocol> *htmlInputToolBarView = (MXKRoomInputToolbarView <HtmlRoomInputToolbarViewProtocol> *) self.inputToolbarView;
        htmlInputToolBarView.htmlContent = self.htmlTextBeforeEditing;
    }
    else if (self.textMessageBeforeEditing && [self inputToolbarConformsToToolbarViewProtocol])
    {
        self.inputToolbarView.attributedTextMessage = self.textMessageBeforeEditing;
    }
    
    self.textMessageBeforeEditing = nil;
    self.htmlTextBeforeEditing = nil;
}

- (BOOL)inputToolbarConformsToHtmlToolbarViewProtocol
{
    return [self.inputToolbarView conformsToProtocol:@protocol(HtmlRoomInputToolbarViewProtocol)];
}

- (BOOL)inputToolbarConformsToToolbarViewProtocol
{
    return [self.inputToolbarView conformsToProtocol:@protocol(RoomInputToolbarViewProtocol)];
}

- (void)showDifferentURLsAlertFor:(NSURL *)url visibleURLString:(NSString *)visibleURLString
{
    //  urls are different, show confirmation alert
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:[VectorL10n externalLinkConfirmationTitle] message:[VectorL10n externalLinkConfirmationMessage:visibleURLString :url.absoluteString] preferredStyle:UIAlertControllerStyleAlert];

    UIAlertAction *continueAction = [UIAlertAction actionWithTitle:[VectorL10n continue] style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        // Try to open the link
        [[UIApplication sharedApplication] vc_open:url completionHandler:^(BOOL success) {
            if (!success)
            {
                [self showUnableToOpenLinkErrorAlert];
            }
        }];
    }];

    UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:[VectorL10n cancel] style:UIAlertActionStyleCancel handler:nil];

    [alert addAction:continueAction];
    [alert addAction:cancelAction];

    [self presentViewController:alert animated:YES completion:nil];
}

#pragma mark - RoomDataSourceDelegate

- (void)roomDataSourceDidUpdateEncryptionTrustLevel:(RoomDataSource *)roomDataSource
{
    [self updateInputToolbarEncryptionDecoration];
    [self updateTitleViewEncryptionDecoration];
}

- (void)roomDataSource:(RoomDataSource *)roomDataSource didTapThread:(id<MXThreadProtocol>)thread
{
    [self openThreadWithId:thread.id];

    [Analytics.shared trackInteraction:AnalyticsUIElementRoomThreadSummaryItem];
}

- (void)roomDataSourceDidUpdateCurrentUserSharingLocationStatus:(RoomDataSource *)roomDataSource
{
    [self updateLiveLocationBannerViewVisibility];
}

#pragma mark - Segues

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    // Keep ref on destinationViewController
    [super prepareForSegue:segue sender:sender];
    
    id pushedViewController = [segue destinationViewController];
    
    if ([[segue identifier] isEqualToString:@"showRoomSearch"])
    {
        // Dismiss keyboard
        [self dismissKeyboard];
        
        RoomSearchViewController* roomSearchViewController = (RoomSearchViewController*)pushedViewController;
        // Add the current data source to be able to search messages.
        roomSearchViewController.roomDataSource = self.roomDataSource;
    }
    else if ([[segue identifier] isEqualToString:@"showContactDetails"])
    {
        if (selectedContact)
        {
            ContactDetailsViewController *contactDetailsViewController = segue.destinationViewController;
            contactDetailsViewController.enableVoipCall = NO;
            contactDetailsViewController.contact = selectedContact;
            
            selectedContact = nil;
        }
    }
    else if ([[segue identifier] isEqualToString:@"showUnknownDevices"])
    {
        if (unknownDevices)
        {
            UsersDevicesViewController *usersDevicesViewController = (UsersDevicesViewController *)segue.destinationViewController.childViewControllers.firstObject;
            [usersDevicesViewController displayUsersDevices:unknownDevices andMatrixSession:self.roomDataSource.mxSession onComplete:nil];
            
            unknownDevices = nil;
        }
    }
}

#pragma mark - VoIP

- (void)placeCallWithVideo:(BOOL)video
{
    __weak __typeof(self) weakSelf = self;
    
    // Check app permissions first
    [MXKTools checkAccessForCall:video
     manualChangeMessageForAudio:[VectorL10n microphoneAccessNotGrantedForCall:AppInfo.current.displayName]
     manualChangeMessageForVideo:[VectorL10n cameraAccessNotGrantedForCall:AppInfo.current.displayName]
       showPopUpInViewController:self completionHandler:^(BOOL granted) {
        
        if (weakSelf)
        {
            typeof(self) self = weakSelf;
            
            if (granted)
            {
                if (video)
                {
                    [self placeCallWithVideo2:video];
                }
                else if (self.mainSession.callManager.supportsPSTN)
                {
                    [self showVoiceCallActionSheet];
                }
                else
                {
                    [self placeCallWithVideo2:NO];
                }
            }
            else
            {
                MXLogDebug(@"RoomViewController: Warning: The application does not have the permission to place the call");
            }
        }
    }];
}

- (void)showVoiceCallActionSheet
{
    // Ask the user the kind of the call: voice or dialpad?
    UIAlertController *callActionSheet = [UIAlertController alertControllerWithTitle:nil
                                                                             message:nil
                                                                      preferredStyle:UIAlertControllerStyleActionSheet];
    
    __weak typeof(self) weakSelf = self;
    [callActionSheet addAction:[UIAlertAction actionWithTitle:[VectorL10n roomPlaceVoiceCall]
                                                        style:UIAlertActionStyleDefault
                                                      handler:^(UIAlertAction * action) {
        
        if (weakSelf)
        {
            typeof(self) self = weakSelf;
            self->currentAlert = nil;
            
            [self placeCallWithVideo2:NO];
        }
        
    }]];
    
    [callActionSheet addAction:[UIAlertAction actionWithTitle:[VectorL10n roomOpenDialpad]
                                                        style:UIAlertActionStyleDefault
                                                      handler:^(UIAlertAction * action) {
        
        if (weakSelf)
        {
            typeof(self) self = weakSelf;
            self->currentAlert = nil;
            
            [self openDialpad];
        }
        
    }]];
    
    [callActionSheet addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
                                                        style:UIAlertActionStyleCancel
                                                      handler:^(UIAlertAction * action) {
        
        if (weakSelf)
        {
            typeof(self) self = weakSelf;
            self->currentAlert = nil;
        }
        
    }]];
    
    [callActionSheet popoverPresentationController].barButtonItem = self.navigationItem.rightBarButtonItems.firstObject;
    [callActionSheet popoverPresentationController].permittedArrowDirections = UIPopoverArrowDirectionUp;
    [self presentViewController:callActionSheet animated:YES completion:nil];
    currentAlert = callActionSheet;
}

- (void)placeCallWithVideo2:(BOOL)video
{
    Widget *jitsiWidget = [self.customizedRoomDataSource jitsiWidget];
    if (jitsiWidget)
    {
        //  If there is already a Jitsi call, join it
        [self showJitsiCallWithWidget:jitsiWidget];
    }
    else
    {
        if (self.roomDataSource.room.summary.membersCount.joined == 2
            && self.roomDataSource.room.isDirect
            && !self.mainSession.vc_homeserverConfiguration.jitsi.useFor1To1Calls)
        {
            //  Matrix call
            [self.roomDataSource.room placeCallWithVideo:video success:nil failure:nil];
        }
        else
        {
            //  Jitsi call
            if (self.canEditJitsiWidget)
            {
                //  User has right to add a Jitsi widget
                //  Create the Jitsi widget and open it directly
                [self startActivityIndicator];
                
                MXWeakify(self);
                
                [[WidgetManager sharedManager] createJitsiWidgetInRoom:self.roomDataSource.room
                                                             withVideo:video
                                                               success:^(Widget *jitsiWidget)
                 {
                    MXStrongifyAndReturnIfNil(self);
                    [self stopActivityIndicator];
                    
                    [self showJitsiCallWithWidget:jitsiWidget];
                }
                                                               failure:^(NSError *error)
                 {
                    MXStrongifyAndReturnIfNil(self);
                    [self stopActivityIndicator];
                    
                    [self showJitsiErrorAsAlert:error];
                }];
            }
            else
            {
                //  Insufficient privileges to add a Jitsi widget
                MXWeakify(self);
                [currentAlert dismissViewControllerAnimated:NO completion:nil];
                
                UIAlertController *unprivilegedAlert = [UIAlertController alertControllerWithTitle:[VectorL10n roomNoPrivilegesToCreateGroupCall]
                                                                                           message:nil
                                                                                    preferredStyle:UIAlertControllerStyleAlert];
                
                [unprivilegedAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok]
                                                                      style:UIAlertActionStyleDefault
                                                                    handler:^(UIAlertAction * action)
                                         {
                    MXStrongifyAndReturnIfNil(self);
                    self->currentAlert = nil;
                }]];
                
                [unprivilegedAlert mxk_setAccessibilityIdentifier:@"RoomVCCallAlert"];
                [self presentViewController:unprivilegedAlert animated:YES completion:nil];
                currentAlert = unprivilegedAlert;
            }
        }
    }
}

- (void)hangupCall
{
    MXCall *callInRoom = [self.roomDataSource.mxSession.callManager callInRoom:self.roomDataSource.roomId];
    if (callInRoom)
    {
        [callInRoom hangup];
    }
    else if (self.isRoomHavingAJitsiCall)
    {
        [self endActiveJitsiCall];
        [self reloadBubblesTable:YES];
    }
    
    [self refreshActivitiesViewDisplay];
    [self refreshRoomInputToolbar];
}

#pragma mark - MXKRoomInputToolbarViewDelegate

- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView isTyping:(BOOL)typing
{
    [super roomInputToolbarView:toolbarView isTyping:typing];

    // TODO: Improve so we don't save partial message twice.
    RoomInputToolbarView *inputToolbar = (RoomInputToolbarView *)toolbarView;

    if (self.saveProgressTextInput && self.roomDataSource && inputToolbar)
    {
        // Store the potential message partially typed in text input
        self.roomDataSource.partialAttributedTextMessage = inputToolbar.attributedTextMessage;
    }

    // Cancel potential selected event (to leave edition mode)
    NSString *selectedEventId = self.customizedRoomDataSource.selectedEventId;
    if (typing && selectedEventId && ![self.roomDataSource canReplyToEventWithId:selectedEventId])
    {
        [self cancelEventSelection];
    }
}

- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView heightDidChanged:(CGFloat)height completion:(void (^)(BOOL finished))completion
{
    if (self.roomInputToolbarContainerHeightConstraint.constant != height)
    {
        [super roomInputToolbarView:toolbarView heightDidChanged:height completion:^(BOOL finished) {
            
            if (completion)
            {
                completion (finished);
            }
        }];
    }
}

- (void)roomInputToolbarViewDidTapCancel:(MXKRoomInputToolbarView<RoomInputToolbarViewProtocol>*)toolbarView
{
    [self cancelEventSelection];
}
 
- (void)roomInputToolbarViewDidChangeTextMessage:(RoomInputToolbarView *)toolbarView
{
    [self.completionSuggestionCoordinator processTextMessage:toolbarView.textMessage];
}

- (void)didDetectTextPattern:(SuggestionPatternWrapper *)suggestionPattern
{
    [self.completionSuggestionCoordinator processSuggestionPattern:suggestionPattern];
}

- (CompletionSuggestionViewModelContextWrapper *)completionSuggestionContext
{
    return [self.completionSuggestionCoordinator sharedContext];
}

- (MXMediaManager *)mediaManager
{
    return self.mainSession.mediaManager;
}

- (void)roomInputToolbarViewDidOpenActionMenu:(RoomInputToolbarView*)toolbarView
{
    // Consider opening the action menu as beginning to type and share encryption keys if requested.
    if ([MXKAppSettings standardAppSettings].outboundGroupSessionKeyPreSharingStrategy == MXKKeyPreSharingWhenTyping)
    {
        [self shareEncryptionKeys];
    }
}

- (void)roomInputToolbarView:(RoomInputToolbarView *)toolbarView sendFormattedTextMessage:(NSString *)formattedTextMessage withRawText:(NSString *)rawText
{
    // Create before sending the message in case of a discussion (direct chat)
    MXWeakify(self);
    [self createDiscussionIfNeeded:^(BOOL readyToSend) {
        MXStrongifyAndReturnIfNil(self);
        
        if (readyToSend) {
            [self sendFormattedTextMessage:rawText htmlMsg:formattedTextMessage];
        }
        // Errors are handled at the request level. This should be improved in case of code rewriting.
    }];
}

- (void)roomInputToolbarView:(MXKRoomInputToolbarView *)toolbarView sendCommand:(NSString *)commandText
{
    // Create before sending the message in case of a discussion (direct chat)
    MXWeakify(self);
    [self createDiscussionIfNeeded:^(BOOL readyToSend) {
        MXStrongifyAndReturnIfNil(self);

        if (readyToSend) {
            if (![self sendAsIRCStyleCommandIfPossible:commandText])
            {
                // Display an error for unknown command
                UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil
                                                                               message:[VectorL10n roomCommandErrorUnknownCommand]
                                                                        preferredStyle:UIAlertControllerStyleAlert];
                [alert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok] style:UIAlertActionStyleDefault handler:nil]];
                [self presentViewController:alert animated:YES completion:nil];
            }
        }
    }];
}

- (void)roomInputToolbarViewShowSendMediaActions:(MXKRoomInputToolbarView *)toolbarView
{
    NSMutableArray *actionItems = [NSMutableArray new];
    if (RiotSettings.shared.roomScreenAllowMediaLibraryAction)
    {
        [actionItems addObject:@(ComposerCreateActionPhotoLibrary)];
    }
    if (RiotSettings.shared.roomScreenAllowStickerAction && !self.isNewDirectChat)
    {
        [actionItems addObject:@(ComposerCreateActionStickers)];
    }
    if (RiotSettings.shared.roomScreenAllowFilesAction)
    {
        [actionItems addObject:@(ComposerCreateActionAttachments)];
    }
    if (RiotSettings.shared.enableVoiceBroadcast && !self.isNewDirectChat)
    {
        [actionItems addObject:@(ComposerCreateActionVoiceBroadcast)];
    }
    if (BuildSettings.pollsEnabled && self.displayConfiguration.sendingPollsEnabled && !self.isNewDirectChat)
    {
        [actionItems addObject:@(ComposerCreateActionPolls)];
    }
    if (BuildSettings.locationSharingEnabled && !self.isNewDirectChat)
    {
        [actionItems addObject:@(ComposerCreateActionLocation)];
    }
    if (RiotSettings.shared.roomScreenAllowCameraAction)
    {
        [actionItems addObject:@(ComposerCreateActionCamera)];
    }
    
    self.composerCreateActionListBridgePresenter = [[ComposerCreateActionListBridgePresenter alloc] initWithActions:actionItems
                                                                                                     wysiwygEnabled:RiotSettings.shared.enableWysiwygComposer
                                                                                              textFormattingEnabled:RiotSettings.shared.enableWysiwygTextFormatting];
    self.composerCreateActionListBridgePresenter.delegate = self;
    [self.composerCreateActionListBridgePresenter presentFrom:self animated:YES];
}

- (void)roomInputToolbarView:(RoomInputToolbarView *)toolbarView sendAttributedTextMessage:(NSAttributedString *)attributedTextMessage
{
    // Create before sending the message in case of a discussion (direct chat)
    MXWeakify(self);
    [self createDiscussionIfNeeded:^(BOOL readyToSend) {
        MXStrongifyAndReturnIfNil(self);
        
        if (readyToSend) {
            BOOL isMessageAHandledCommand = NO;
            // "/me" command is supported with Pills in RoomDataSource.
            if (![attributedTextMessage.string hasPrefix:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandEmote]])
            {
                // Other commands currently work with identifiers (e.g. ban, invite, op, etc).
                NSString *message;
                if (@available(iOS 15.0, *))
                {
                    message = [PillsFormatter stringByReplacingPillsIn:attributedTextMessage mode:PillsReplacementTextModeIdentifier];
                }
                else
                {
                    message = attributedTextMessage.string;
                }
                // Try to send the slash command
                isMessageAHandledCommand = [self sendAsIRCStyleCommandIfPossible:message];
            }
            
            if (!isMessageAHandledCommand)
            {
                [self sendAttributedTextMessage:attributedTextMessage];
            }
        }
        // Errors are handled at the request level. This should be improved in case of code rewriting.
    }];
}

- (void)roomInputToolbarView:(MXKRoomInputToolbarView *)toolbarView shouldStorePartialContent:(NSAttributedString *)partialAttributedTextMessage
{
    self.roomDataSource.partialAttributedTextMessage = partialAttributedTextMessage;
}

#pragma mark - MXKRoomMemberDetailsViewControllerDelegate

- (void)roomMemberDetailsViewController:(MXKRoomMemberDetailsViewController *)roomMemberDetailsViewController startChatWithMemberId:(NSString *)matrixId completion:(void (^)(void))completion
{
    [self startChatWithUserId:matrixId completion:completion];
}

- (void)roomMemberDetailsViewController:(MXKRoomMemberDetailsViewController *)roomMemberDetailsViewController mention:(MXRoomMember*)member
{
    [self mention:member];
}

#pragma mark - Action

- (IBAction)onVoiceCallPressed:(id)sender
{
    // Manage case of a Voice broadcast listening -> Pause Voice broadcast playback
    [VoiceBroadcastPlaybackProvider.shared pausePlaying];
    
    if (VoiceBroadcastRecorderProvider.shared.isVoiceBroadcastRecording) {
        [[AppDelegate theDelegate] showAlertWithTitle:VectorL10n.voiceBroadcastVoipCannotStartTitle
                                              message:VectorL10n.voiceBroadcastVoipCannotStartDescription];
    }
    else if (self.isCallActive)
    {
        [self hangupCall];
    }
    else
    {
        [self placeCallWithVideo:NO];
    }
}

- (IBAction)onVideoCallPressed:(id)sender
{
    // Manage case of a Voice broadcast listening -> Pause Voice broadcast playback
    [VoiceBroadcastPlaybackProvider.shared pausePlaying];

    if (VoiceBroadcastRecorderProvider.shared.isVoiceBroadcastRecording) {
        [[AppDelegate theDelegate] showAlertWithTitle:VectorL10n.voiceBroadcastVoipCannotStartTitle
                                              message:VectorL10n.voiceBroadcastVoipCannotStartDescription];
    } else {
        [self placeCallWithVideo:YES];
    }
}

- (IBAction)onThreadListTapped:(id)sender
{
    self.threadsBridgePresenter = [self.delegate threadsCoordinatorForRoomViewController:self threadId:nil];
    self.threadsBridgePresenter.delegate = self;
    [self.threadsBridgePresenter pushFrom:self.navigationController animated:YES];

    [Analytics.shared trackInteraction:AnalyticsUIElementRoomThreadListButton];
}

- (IBAction)onIntegrationsPressed:(id)sender
{
    WidgetPickerViewController *widgetPicker = [[WidgetPickerViewController alloc] initForMXSession:self.roomDataSource.mxSession
                                                                                             inRoom:self.roomDataSource.roomId];
    
    [widgetPicker showInViewController:self];
}

- (void)scrollToBottomAction:(id)sender
{
    [self goBackToLive];
}

- (IBAction)onButtonPressed:(id)sender
{
    if (sender == self.jumpToLastUnreadButton)
    {
        // Dismiss potential keyboard.
        [self dismissKeyboard];
        NSString *eventId = self.roomDataSource.room.accountData.readMarkerEventId;
        NSString *threadId = self.roomDataSource.threadId;
        [self reloadRoomWihtEventId:eventId threadId:threadId forceUpdateRoomMarker:YES];
    }
    else if (sender == self.resetReadMarkerButton)
    {
        // Move the read marker to the current read receipt position.
        [self.roomDataSource.room forgetReadMarker];
        
        // Hide the banner
        self.jumpToLastUnreadBannerContainer.hidden = YES;
    }
}

- (void)handleReportRoom 
{
    // Prompt user to enter a description of the problem content.
    UIAlertController *reportReasonAlert = [UIAlertController alertControllerWithTitle:[VectorL10n roomActionReportPromptReason]
                                                                               message:nil
                                                                        preferredStyle:UIAlertControllerStyleAlert];
    
    [reportReasonAlert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
        textField.secureTextEntry = NO;
        textField.placeholder = nil;
        textField.keyboardType = UIKeyboardTypeDefault;
    }];
    
    MXWeakify(self);
    [reportReasonAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
        MXStrongifyAndReturnIfNil(self);
        
        NSString *text = [self->currentAlert textFields].firstObject.text;
        self->currentAlert = nil;
        
        [self startActivityIndicator];
        
        [self.roomDataSource.mxSession.matrixRestClient reportRoom:self.roomDataSource.roomId reason:text success:^{
            MXStrongifyAndReturnIfNil(self);
            [self stopActivityIndicator];
        } failure:^(NSError *error) {
            MXStrongifyAndReturnIfNil(self);
            [self stopActivityIndicator];
            
            MXLogDebug(@"[RoomVC] Report room (%@) failed", self.roomDataSource.roomId);
            //Alert user
            [self showError:error];
        }];
    }]];
    
    [reportReasonAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) {
        MXStrongifyAndReturnIfNil(self);
        self->currentAlert = nil;
    }]];
    
    [self presentViewController:reportReasonAlert animated:YES completion:nil];
    self->currentAlert = reportReasonAlert;
}

#pragma mark - UITableViewDelegate

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    cell.backgroundColor = ThemeService.shared.theme.backgroundColor;
    
    // Update the selected background view
    if (ThemeService.shared.theme.selectedBackgroundColor)
    {
        cell.selectedBackgroundView = [[UIView alloc] init];
        cell.selectedBackgroundView.backgroundColor = ThemeService.shared.theme.selectedBackgroundColor;
    }
    else
    {
        if (tableView.style == UITableViewStylePlain)
        {
            cell.selectedBackgroundView = nil;
        }
        else
        {
            cell.selectedBackgroundView.backgroundColor = nil;
        }
    }
    
    if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class])
    {
        MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell*)cell;
        if (roomBubbleTableViewCell.readMarkerView)
        {
            readMarkerTableViewCell = roomBubbleTableViewCell;
            
            dispatch_async(dispatch_get_main_queue(), ^{
                [self checkReadMarkerVisibility];
            });
        }
    }
}

- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath
{
    if (cell == readMarkerTableViewCell)
    {
        readMarkerTableViewCell = nil;
    }
    
    [super tableView:tableView didEndDisplayingCell:cell forRowAtIndexPath:indexPath];
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [super tableView:tableView didSelectRowAtIndexPath:indexPath];
}

#pragma mark -

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    [super scrollViewDidScroll:scrollView];
    
    [self checkReadMarkerVisibility];
    
    // Switch back to the live mode when the user scrolls to the bottom of the non live timeline.
    if (!self.roomDataSource.isLive && ![self isRoomPreview] && !self.isNewDirectChat)
    {
        CGFloat contentBottomPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.frame.size.height - self.bubblesTableView.adjustedContentInset.bottom;
        if (contentBottomPosY >= self.bubblesTableView.contentSize.height && ![self.roomDataSource.timeline canPaginate:MXTimelineDirectionForwards])
        {
            [self goBackToLive];
        }
    }
}

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
    if ([MXKRoomViewController instancesRespondToSelector:@selector(scrollViewWillBeginDragging:)])
    {
        [super scrollViewWillBeginDragging:scrollView];
    }
    
    [self cancelEventHighlight];
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    if ([MXKRoomViewController instancesRespondToSelector:@selector(scrollViewDidEndDragging:willDecelerate:)])
    {
        [super scrollViewDidEndDragging:scrollView willDecelerate:decelerate];
    }
    
    if (decelerate == NO)
    {
        // Handle swipe on expanded header
        [self onScrollViewDidEndScrolling:scrollView];
        
        [self refreshActivitiesViewDisplay];
        [self refreshJumpToLastUnreadBannerDisplay];
    }
    else
    {
        // Dispatch async the expanded header handling in order to let the deceleration go first.
        dispatch_async(dispatch_get_main_queue(), ^{
            
            // Handle swipe on expanded header
            [self onScrollViewDidEndScrolling:scrollView];
            
        });
    }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    if ([MXKRoomViewController instancesRespondToSelector:@selector(scrollViewDidEndDecelerating:)])
    {
        [super scrollViewDidEndDecelerating:scrollView];
    }
    
    [self refreshActivitiesViewDisplay];
    [self refreshJumpToLastUnreadBannerDisplay];
}

- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
{
    if ([MXKRoomViewController instancesRespondToSelector:@selector(scrollViewDidEndScrollingAnimation:)])
    {
        [super scrollViewDidEndScrollingAnimation:scrollView];
    }
    
    [self refreshActivitiesViewDisplay];
    [self refreshJumpToLastUnreadBannerDisplay];
}

- (void)onScrollViewDidEndScrolling:(UIScrollView *)scrollView
{
    
}

#pragma mark - MXKRoomTitleViewDelegate

- (BOOL)roomTitleViewShouldBeginEditing:(MXKRoomTitleView*)titleView
{
    // Disable room name edition
    return NO;
}

#pragma mark - RoomTitleViewTapGestureDelegate

- (void)roomTitleView:(RoomTitleView*)titleView recognizeTapGesture:(UITapGestureRecognizer*)tapGestureRecognizer
{
    UIView *tappedView = tapGestureRecognizer.view;
    
    if (tappedView == titleView.titleMask)
    {
        [self showRoomInfo];
    }
    else if (tappedView == previewHeader.rightButton)
    {
        // 'Join' button has been pressed
        if (!roomPreviewData)
        {
            [self joinRoom:^(MXKRoomViewControllerJoinRoomResult result) {
                switch (result)
                {
                    case MXKRoomViewControllerJoinRoomResultSuccess:
                        [self refreshRoomTitle];
                        break;
                    case MXKRoomViewControllerJoinRoomResultFailureRoomEmpty:
                        [self declineRoomInvitation];
                        break;
                    default:
                        break;
                }
            }];
            
            return;
        }
        
        // Attempt to join the room (keep reference on the potential eventId, the preview data will be removed automatically in case of success).
        NSString *eventId = roomPreviewData.eventId;
        
        // We promote here join by room alias instead of room id when an alias is available.
        NSString *roomIdOrAlias = roomPreviewData.roomId;
        
        if (roomPreviewData.roomCanonicalAlias.length)
        {
            roomIdOrAlias = roomPreviewData.roomCanonicalAlias;
        }
        else if (roomPreviewData.roomAliases.count)
        {
            roomIdOrAlias = roomPreviewData.roomAliases.firstObject;
        }
        
        // Note in case of simple link to a room the signUrl param is nil
        [self joinRoomWithRoomIdOrAlias:roomIdOrAlias viaServers:roomPreviewData.viaServers
                             andSignUrl:roomPreviewData.emailInvitation.signUrl
                             completion:^(MXKRoomViewControllerJoinRoomResult result) {
            
            switch (result)
            {
                case MXKRoomViewControllerJoinRoomResultSuccess:
                {
                    // If an event was specified, replace the datasource by a non live datasource showing the event
                    if (eventId)
                    {
                        MXWeakify(self);
                        [RoomDataSource loadRoomDataSourceWithRoomId:self.roomDataSource.roomId
                                                      initialEventId:eventId
                                                            threadId:self.roomDataSource.threadId
                                                    andMatrixSession:self.mainSession
                                                          onComplete:^(id roomDataSource) {
                            MXStrongifyAndReturnIfNil(self);
                            
                            [roomDataSource finalizeInitialization];
                            ((RoomDataSource*)roomDataSource).markTimelineInitialEvent = YES;
                            
                            [self displayRoom:roomDataSource];
                            
                            self.hasRoomDataSourceOwnership = YES;
                        }];
                    }
                    else
                    {
                        // Enable back the text input
                        [self setRoomInputToolbarViewClass:[RoomViewController mainToolbarClass]];
                        [self updateInputToolBarViewHeight];
                        
                        // And the extra area
                        [self setRoomActivitiesViewClass:RoomActivitiesView.class];
                        
                        [self refreshRoomTitle];
                        [self refreshRoomInputToolbar];
                    }
                    break;
                }
                case MXKRoomViewControllerJoinRoomResultFailureRoomEmpty:
                    [self declineRoomInvitation];
                    break;
                default:
                    break;
            }
        }];
    }
    else if (tappedView == previewHeader.leftButton)
    {
        [self presentDeclineOptionsFromView:tappedView];
    }
    else if (tappedView == previewHeader.reportButton)
    {
        [self handleReportRoom];
    }
}

- (void)presentDeclineOptionsFromView:(UIView *)view
{
    UIAlertController *actionSheet = [UIAlertController alertControllerWithTitle:[VectorL10n roomPreviewDeclineInvitationOptions]
                                                                         message:nil
                                                                  preferredStyle:UIAlertControllerStyleActionSheet];
    [actionSheet addAction:[UIAlertAction actionWithTitle:[VectorL10n decline]
                                                    style:UIAlertActionStyleDefault
                                                  handler:^(UIAlertAction * _Nonnull action) {
        [self declineRoomInvitation];
    }]];
    [actionSheet addAction:[UIAlertAction actionWithTitle:[VectorL10n ignoreUser]
                                                    style:UIAlertActionStyleDestructive
                                                  handler:^(UIAlertAction * _Nonnull action) {
        [self ignoreInviteSender];
    }]];
    [actionSheet addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
                                                    style:UIAlertActionStyleCancel
                                                  handler:nil]];
    actionSheet.popoverPresentationController.sourceView = view;
    [self presentViewController:actionSheet animated:YES completion:nil];
}

- (void)declineRoomInvitation
{
    // 'Decline' button has been pressed
    if (roomPreviewData)
    {
        [self roomPreviewDidTapCancelAction];
    }
    else
    {
        [self startActivityIndicator];
        MXWeakify(self);
        [self.roomDataSource.room leave:^{
            MXStrongifyAndReturnIfNil(self);
            
            [self stopActivityIndicator];
            [self popToHomeViewController];
            
        } failure:^(NSError *error) {
            MXStrongifyAndReturnIfNil(self);
            
            [self stopActivityIndicator];
            MXLogDebug(@"[RoomVC] Failed to reject an invited room (%@) failed", self.roomDataSource.room.roomId);
            
        }];
    }
}

- (void)ignoreInviteSender
{
    [self startActivityIndicator];
    MXWeakify(self);
    [self.roomDataSource.room ignoreInviteSender:^{
        MXStrongifyAndReturnIfNil(self);
        
        [self stopActivityIndicator];
        [self popToHomeViewController];

    } failure:^(NSError *error) {
        MXStrongifyAndReturnIfNil(self);
        
        [self stopActivityIndicator];
        MXLogDebug(@"[RoomVC] Failed to ignore inviter in room (%@)", self.roomDataSource.room.roomId);
    }];
}

- (void)popToHomeViewController
{
    // We remove the current view controller.
    // Pop to homes view controller
    [[AppDelegate theDelegate] restoreInitialDisplay:^{}];
}

#pragma mark - Typing management

- (void)removeTypingNotificationsListener
{
    if (self.roomDataSource)
    {
        // Remove the previous live listener
        if (typingNotifListener)
        {
            MXWeakify(self);
            [self.roomDataSource.room liveTimeline:^(id<MXEventTimeline> liveTimeline) {
                MXStrongifyAndReturnIfNil(self);
                
                [liveTimeline removeListener:self->typingNotifListener];
                self->typingNotifListener = nil;
            }];
        }
    }
    
    currentTypingUsers = nil;
}

- (void)listenTypingNotifications
{
    if (self.roomDataSource)
    {
        // Add typing notification listener
        MXWeakify(self);
        self->typingNotifListener = [self.roomDataSource.room listenToEventsOfTypes:@[kMXEventTypeStringTypingNotification] onEvent:^(MXEvent *event, MXTimelineDirection direction, MXRoomState *roomState) {
            MXStrongifyAndReturnIfNil(self);
            
            // Handle only live events
            if (direction == MXTimelineDirectionForwards)
            {
                // Retrieve typing users list
                NSMutableArray *typingUsers = [NSMutableArray arrayWithArray:self.roomDataSource.room.typingUsers];
                // Remove typing info for the current user
                NSUInteger index = [typingUsers indexOfObject:self.mainSession.myUser.userId];
                if (index != NSNotFound)
                {
                    [typingUsers removeObjectAtIndex:index];
                }
                
                // Ignore this notification if both arrays are empty
                if (self->currentTypingUsers.count || typingUsers.count)
                {
                    self->currentTypingUsers = typingUsers;
                    [self refreshActivitiesViewDisplay];
                }
            }
        }];
        
        // Retrieve the current typing users list
        NSMutableArray *typingUsers = [NSMutableArray arrayWithArray:self.roomDataSource.room.typingUsers];
        // Remove typing info for the current user
        NSUInteger index = [typingUsers indexOfObject:self.mainSession.myUser.userId];
        if (index != NSNotFound)
        {
            [typingUsers removeObjectAtIndex:index];
        }
        currentTypingUsers = typingUsers;
        [self refreshActivitiesViewDisplay];
    }
}

- (void)refreshTypingNotification
{
    RoomDataSource *roomDataSource = (RoomDataSource *) self.roomDataSource;
    BOOL needsUpdate = currentTypingUsers.count != roomDataSource.currentTypingUsers.count;

    NSMutableArray *typingUsers = [NSMutableArray new];
    for (NSUInteger i = 0 ; i < currentTypingUsers.count ; i++) {
        NSString *userId = currentTypingUsers[i];
        MXRoomMember* member = [self.roomDataSource.roomState.members memberWithUserId:userId];
        TypingUserInfo *userInfo;
        if (member)
        {
            userInfo = [[TypingUserInfo alloc] initWithMember: member];
        }
        else
        {
            userInfo = [[TypingUserInfo alloc] initWithUserId: userId];
        }
        [typingUsers addObject:userInfo];
        needsUpdate = needsUpdate || userInfo.userId != ((MXRoomMember *) roomDataSource.currentTypingUsers[i]).userId;
    }

    if (needsUpdate)
    {
//        BOOL needsReload = roomDataSource.currentTypingUsers == nil;
        // Quick fix for https://github.com/vector-im/element-ios/issues/4230
        BOOL needsReload = YES;
        roomDataSource.currentTypingUsers = typingUsers;
        if (needsReload)
        {
            [self.bubblesTableView reloadData];
        }
        else
        {
            NSInteger count = [self.bubblesTableView numberOfRowsInSection:0];
            NSIndexPath *lastIndexPath = [NSIndexPath indexPathForRow:count - 1 inSection:0];
            [self.bubblesTableView reloadRowsAtIndexPaths:@[lastIndexPath] withRowAnimation:UITableViewRowAnimationFade];
        }
        
        if (self.isScrollToBottomHidden
            && !self.bubblesTableView.isDragging
            && !self.bubblesTableView.isDecelerating)
        {
            NSInteger count = [self.bubblesTableView numberOfRowsInSection:0];
            if (count)
            {
                [self scrollBubblesTableViewToBottomAnimated:YES];
            }
        }
    }
}

#pragma mark - Call notifications management

- (void)removeCallNotificationsListeners
{
    if (kMXCallStateDidChangeObserver)
    {
        [[NSNotificationCenter defaultCenter] removeObserver:kMXCallStateDidChangeObserver];
        kMXCallStateDidChangeObserver = nil;
    }
    if (kMXCallManagerConferenceStartedObserver)
    {
        [[NSNotificationCenter defaultCenter] removeObserver:kMXCallManagerConferenceStartedObserver];
        kMXCallManagerConferenceStartedObserver = nil;
    }
    if (kMXCallManagerConferenceFinishedObserver)
    {
        [[NSNotificationCenter defaultCenter] removeObserver:kMXCallManagerConferenceFinishedObserver];
        kMXCallManagerConferenceFinishedObserver = nil;
    }
}

- (void)listenCallNotifications
{
    MXWeakify(self);
    
    kMXCallStateDidChangeObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXCallStateDidChange object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
        
        MXStrongifyAndReturnIfNil(self);
        
        MXCall *call = notif.object;
        if ([call.room.roomId isEqualToString:self.customizedRoomDataSource.roomId])
        {
            [self refreshActivitiesViewDisplay];
            [self refreshRoomInputToolbar];
        }
    }];
    kMXCallManagerConferenceStartedObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXCallManagerConferenceStarted object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
        
        MXStrongifyAndReturnIfNil(self);
        
        NSString *roomId = notif.object;
        if ([roomId isEqualToString:self.customizedRoomDataSource.roomId])
        {
            [self refreshActivitiesViewDisplay];
        }
    }];
    kMXCallManagerConferenceFinishedObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXCallManagerConferenceFinished object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
        
        MXStrongifyAndReturnIfNil(self);
        
        NSString *roomId = notif.object;
        if ([roomId isEqualToString:self.customizedRoomDataSource.roomId])
        {
            [self refreshActivitiesViewDisplay];
            [self refreshRoomInputToolbar];
        }
    }];
}


#pragma mark - Server notices management

- (void)removeServerNoticesListener
{
    if (serverNotices)
    {
        [serverNotices close];
        serverNotices = nil;
    }
}

- (void)listenToServerNotices
{
    if (!serverNotices)
    {
        serverNotices = [[MXServerNotices alloc] initWithMatrixSession:self.roomDataSource.mxSession];
        serverNotices.delegate = self;
    }
}

- (void)serverNoticesDidChangeState:(MXServerNotices *)serverNotices
{
    [self refreshActivitiesViewDisplay];
}

#pragma mark - Widget notifications management

- (void)removeWidgetNotificationsListeners
{
    if (kMXKWidgetManagerDidUpdateWidgetObserver)
    {
        [[NSNotificationCenter defaultCenter] removeObserver:kMXKWidgetManagerDidUpdateWidgetObserver];
        kMXKWidgetManagerDidUpdateWidgetObserver = nil;
    }
}

- (void)listenWidgetNotifications
{
    if (!self.displayConfiguration.jitsiWidgetRemoverEnabled)
    {
        return;
    }
    
    MXWeakify(self);
    
    kMXKWidgetManagerDidUpdateWidgetObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kWidgetManagerDidUpdateWidgetNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
        
        MXStrongifyAndReturnIfNil(self);
        
        Widget *widget = notif.object;
        if (widget.mxSession == self.roomDataSource.mxSession
            && [widget.roomId isEqualToString:self.customizedRoomDataSource.roomId])
        {
            //  Call button update
            [self refreshRoomTitle];
            //  Remove Jitsi widget view update
            [self refreshRemoveJitsiWidgetView];
        }
    }];
}

- (void)showJitsiErrorAsAlert:(NSError*)error
{
    // Customise the error for permission issues
    if ([error.domain isEqualToString:WidgetManagerErrorDomain] && error.code == WidgetManagerErrorCodeNotEnoughPower)
    {
        error = [NSError errorWithDomain:error.domain
                                    code:error.code
                                userInfo:@{
                                    NSLocalizedDescriptionKey: [VectorL10n roomConferenceCallNoPower]
                                }];
    }
    
    // Alert user
    [self showError:error];
}

- (NSUInteger)widgetsCount:(BOOL)includeUserWidgets
{
    if (!self.displayConfiguration.integrationsEnabled)
    {
        return 0;
    }
    
    NSUInteger widgetsCount = [[WidgetManager sharedManager] widgetsNotOfTypes:@[kWidgetTypeJitsiV1, kWidgetTypeJitsiV2]
                                                                        inRoom:self.roomDataSource.room
                                                                 withRoomState:self.roomDataSource.roomState].count;
    if (includeUserWidgets)
    {
        widgetsCount += [[WidgetManager sharedManager] userWidgets:self.roomDataSource.room.mxSession].count;
    }
    
    return widgetsCount;
}

#pragma mark - Unreachable Network Handling

- (void)refreshActivitiesViewDisplay
{
    if ([self.activitiesView isKindOfClass:RoomActivitiesView.class])
    {
        RoomActivitiesView *roomActivitiesView = (RoomActivitiesView*)self.activitiesView;
        
        // Reset gesture recognizers
        while (roomActivitiesView.gestureRecognizers.count)
        {
            [roomActivitiesView removeGestureRecognizer:roomActivitiesView.gestureRecognizers[0]];
        }
        
        if ([self.roomDataSource.mxSession.syncError.errcode isEqualToString:kMXErrCodeStringResourceLimitExceeded])
        {
            self.activitiesViewExpanded = YES;
            [roomActivitiesView showResourceLimitExceededError:self.roomDataSource.mxSession.syncError.userInfo onAdminContactTapped:^(NSURL *adminContactURL) {
                [[UIApplication sharedApplication] vc_open:adminContactURL completionHandler:^(BOOL success) {
                    if (!success)
                    {
                        MXLogDebug(@"[RoomVC] refreshActivitiesViewDisplay: adminContact(%@) cannot be opened", adminContactURL);
                    }
                }];
            }];
        }
        else if ([AppDelegate theDelegate].isOffline)
        {
            // Doing nothing here as the offline notification is now handled by the AppCoordinator
        }
        else if (self.customizedRoomDataSource.roomState.isObsolete)
        {
            self.activitiesViewExpanded = YES;
            MXWeakify(self);
            [roomActivitiesView displayRoomReplacementWithRoomLinkTappedHandler:^{
                MXStrongifyAndReturnIfNil(self);
                
                MXEvent *stoneTombEvent = [self.customizedRoomDataSource.roomState stateEventsWithType:kMXEventTypeStringRoomTombStone].lastObject;
                
                NSString *replacementRoomId = self.customizedRoomDataSource.roomState.tombStoneContent.replacementRoomId;
                if ([self.roomDataSource.mxSession roomWithRoomId:replacementRoomId])
                {
                    // Open the room if it is already joined
                    [self showRoomWithId:replacementRoomId];
                }
                else
                {
                    // Else auto join it via the server that sent the event
                    MXLogDebug(@"[RoomVC] Auto join an upgraded room: %@ -> %@. Sender: %@",                              self.customizedRoomDataSource.roomState.roomId,
                          replacementRoomId, stoneTombEvent.sender);
                    
                    NSString *viaSenderServer = [MXTools serverNameInMatrixIdentifier:stoneTombEvent.sender];
                    
                    if (viaSenderServer)
                    {
                        [self startActivityIndicator];
                        [self.roomDataSource.mxSession joinRoom:replacementRoomId viaServers:@[viaSenderServer] success:^(MXRoom *room) {
                            [self stopActivityIndicator];

                            [self showRoomWithId:replacementRoomId];
                            
                        } failure:^(NSError *error) {
                            [self stopActivityIndicator];
                            
                            MXLogDebug(@"[RoomVC] Failed to join an upgraded room. Error: %@",
                                  error);
                            [self showError:error];
                        }];
                    }
                }
            }];
        }
        else if ([self checkUnsentMessages] == NO)
        {
            // Show "scroll to bottom" icon when the most recent message is not visible,
            // or when the timelime is not live (this icon is used to go back to live).
            // Note: we check if `currentEventIdAtTableBottom` is set to know whether the table has been rendered at least once.
            if (!self.roomDataSource.isLive || (currentEventIdAtTableBottom && [self isBubblesTableScrollViewAtTheBottom] == NO))
            {
                if (self.roomDataSource.room)
                {
                    // Retrieve the unread messages count on the current thread
                    NSUInteger unreadCount = [self.mainSession.store
                                              localUnreadEventCount:self.roomDataSource.room.roomId
                                              threadId:self.roomDataSource.threadId ?: kMXEventTimelineMain
                                              withTypeIn:self.mainSession.unreadEventTypes];
                    
                    self.scrollToBottomBadgeLabel.text = unreadCount ? [NSString stringWithFormat:@"%lu", unreadCount] : nil;
                    self.scrollToBottomHidden = NO;
                }
                else
                {
                    //  will be here for left rooms
                    self.scrollToBottomBadgeLabel.text = nil;
                    self.scrollToBottomHidden = YES;
                }
            }
            else if (serverNotices.usageLimit && serverNotices.usageLimit.isServerNoticeUsageLimit)
            {
                self.scrollToBottomHidden = YES;
                self.activitiesViewExpanded = YES;
                [roomActivitiesView showResourceUsageLimitNotice:serverNotices.usageLimit onAdminContactTapped:^(NSURL *adminContactURL) {
                    [[UIApplication sharedApplication] vc_open:adminContactURL completionHandler:^(BOOL success) {
                        if (!success)
                        {
                            MXLogDebug(@"[RoomVC] refreshActivitiesViewDisplay: adminContact(%@) cannot be opened", adminContactURL);
                        }
                    }];
                }];
            }
            else
            {
                self.scrollToBottomHidden = YES;
                self.activitiesViewExpanded = NO;
                [self refreshTypingNotification];
            }
        }
        
        // Recognize swipe downward to dismiss keyboard if any
        UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(onSwipeGesture:)];
        [swipe setNumberOfTouchesRequired:1];
        [swipe setDirection:UISwipeGestureRecognizerDirectionDown];
        [roomActivitiesView addGestureRecognizer:swipe];
    }
}

- (void)goBackToLive
{
    if (self.roomDataSource.isLive)
    {
        // Enable the read marker display, and disable its update (in order to not mark as read all the new messages by default).
        self.roomDataSource.showReadMarker = YES;
        self.updateRoomReadMarker = NO;
        
        [self scrollBubblesTableViewToBottomAnimated:YES];

        [self cancelEventHighlight];
    }
    else
    {
        MXWeakify(self);

        void(^continueBlock)(MXKRoomDataSource *, BOOL) = ^(MXKRoomDataSource *roomDataSource, BOOL hasRoomDataSourceOwnership){
            MXStrongifyAndReturnIfNil(self);

            [roomDataSource finalizeInitialization];

            // Scroll to bottom the bubble history on the display refresh.
            self->shouldScrollToBottomOnTableRefresh = YES;

            [self displayRoom:roomDataSource];

            // Set the room view controller has the data source ownership here.
            self.hasRoomDataSourceOwnership = hasRoomDataSourceOwnership;

            [self refreshActivitiesViewDisplay];
            [self refreshJumpToLastUnreadBannerDisplay];

            if (self.saveProgressTextInput)
            {
                // Restore the potential message partially typed before jump to last unread messages.
                [self.inputToolbarView setPartialContent:roomDataSource.partialAttributedTextMessage];
            }
        };

        if (self.roomDataSource.threadId)
        {
            [ThreadDataSource loadRoomDataSourceWithRoomId:self.roomDataSource.roomId
                                            initialEventId:nil
                                                  threadId:self.roomDataSource.threadId
                                          andMatrixSession:self.mainSession
                                                onComplete:^(ThreadDataSource *threadDataSource)
             {
                continueBlock(threadDataSource, YES);
            }];
        }
        else if (self.roomDataSource.roomId)
        {
            if (self.isContextPreview)
            {
                [RoomPreviewDataSource loadRoomDataSourceWithRoomId:self.roomDataSource.roomId
                                                           threadId:nil
                                                   andMatrixSession:self.mainSession
                                                         onComplete:^(RoomPreviewDataSource *roomDataSource)
                 {
                    continueBlock(roomDataSource, YES);
                }];
            }
            else
            {
                // Switch back to the room live timeline managed by MXKRoomDataSourceManager
                MXKRoomDataSourceManager *roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:self.mainSession];

                [roomDataSourceManager roomDataSourceForRoom:self.roomDataSource.roomId
                                                      create:YES
                                                  onComplete:^(MXKRoomDataSource *roomDataSource) {
                    continueBlock(roomDataSource, NO);
                }];
            }
        }
    }
}

#pragma mark - Missed discussions handling

- (void)refreshMissedDiscussionsCount:(BOOL)force
{
    // Ignore this action when no room is displayed
    if (!self.showMissedDiscussionsBadge || !self.roomDataSource || !missedDiscussionsBadgeLabel
        || [UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPhone
        || ([[UIScreen mainScreen] nativeBounds].size.height > 2532 && UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)))
    {
        self.missedDiscussionsBadgeHidden = YES;
        return;
    }
    
    self.missedDiscussionsBadgeHidden = NO;

    NSUInteger highlightCount = 0;
    NSUInteger missedCount = [[AppDelegate theDelegate].masterTabBarController missedDiscussionsCount];
    
    // Compute the missed notifications count of the current room by considering its notification mode in Riot.
    NSUInteger roomNotificationCount = self.roomDataSource.room.summary.notificationCount;
    if (self.roomDataSource.room.isMentionsOnly)
    {
        // Only the highlighted missed messages must be considered here.
        roomNotificationCount = self.roomDataSource.room.summary.highlightCount;
    }
    
    // Remove the current room from the missed discussion counter.
    if (missedCount && roomNotificationCount)
    {
        missedCount--;
    }
    
    if (missedCount)
    {
        // Compute the missed highlight count
        highlightCount = [[AppDelegate theDelegate].masterTabBarController missedHighlightDiscussionsCount];
        if (highlightCount && self.roomDataSource.room.summary.highlightCount)
        {
            // Remove the current room from the missed highlight counter
            highlightCount--;
        }
    }
    
    if (force || missedDiscussionsCount != missedCount || missedHighlightCount != highlightCount)
    {
        missedDiscussionsCount = missedCount;
        missedHighlightCount = highlightCount;
        
        if (missedCount)
        {
            // Refresh missed discussions count label
            if (missedCount > 99)
            {
                missedDiscussionsBadgeLabel.text = @"99+";
            }
            else
            {
                missedDiscussionsBadgeLabel.text = [NSString stringWithFormat:@"%tu", missedCount];
            }
            
            missedDiscussionsDotView.alpha = highlightCount == 0 ? 0 : 1;
        }
        else
        {
            missedDiscussionsBadgeLabel.text = nil;
        }
    }
}

#pragma mark - Unsent Messages Handling

-(BOOL)checkUnsentMessages
{
    MXRoomSummarySentStatus sentStatus = MXRoomSummarySentStatusOk;
    if ([self.activitiesView isKindOfClass:RoomActivitiesView.class])
    {
        sentStatus = self.roomDataSource.room.summary.sentStatus;
        
        if (sentStatus != MXRoomSummarySentStatusOk)
        {
            NSString *notification = sentStatus == MXRoomSummarySentStatusSentFailedDueToUnknownDevices ?
            [VectorL10n roomUnsentMessagesUnknownDevicesNotification] :
            [VectorL10n roomUnsentMessagesNotification];
            
            MXWeakify(self);
            RoomActivitiesView *roomActivitiesView = (RoomActivitiesView*) self.activitiesView;
            self.activitiesViewExpanded = YES;
            [roomActivitiesView displayUnsentMessagesNotification:notification withResendLink:^{
                
                [self resendAllUnsentMessages];
                
            } andCancelLink:^{
                
                [self cancelAllUnsentMessages];
                
            } andIconTapGesture:^{
                MXStrongifyAndReturnIfNil(self);
                
                if (self->currentAlert)
                {
                    [self->currentAlert dismissViewControllerAnimated:NO completion:nil];
                }
                
                MXWeakify(self);
                UIAlertController *resendAlert = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
                
                [resendAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomResendUnsentMessages]
                                                                style:UIAlertActionStyleDefault
                                                              handler:^(UIAlertAction * action) {
                    
                    MXStrongifyAndReturnIfNil(self);
                    
                    [self resendAllUnsentMessages];
                    self->currentAlert = nil;
                    
                }]];
                
                [resendAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomDeleteUnsentMessages]
                                                                style:UIAlertActionStyleDefault
                                                              handler:^(UIAlertAction * action) {
                    
                    MXStrongifyAndReturnIfNil(self);
                    
                    [self cancelAllUnsentMessages];
                    self->currentAlert = nil;
                    
                }]];
                
                [resendAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
                                                                style:UIAlertActionStyleCancel
                                                              handler:^(UIAlertAction * action) {
                    
                    MXStrongifyAndReturnIfNil(self);
                    
                    self->currentAlert = nil;
                    
                }]];
                
                [resendAlert mxk_setAccessibilityIdentifier:@"RoomVCUnsentMessagesMenuAlert"];
                [resendAlert popoverPresentationController].sourceView = roomActivitiesView;
                [resendAlert popoverPresentationController].sourceRect = roomActivitiesView.bounds;
                [self presentViewController:resendAlert animated:YES completion:nil];
                self->currentAlert = resendAlert;
                
            }];
        }
    }
    
    return sentStatus != MXRoomSummarySentStatusOk;
}

- (void)eventDidChangeSentState:(NSNotification *)notif
{
    // We are only interested by event that has just failed in their encryption
    // because of unknown devices in the room
    MXEvent *event = notif.object;
    if (event.sentState == MXEventSentStateFailed &&
        [event.roomId isEqualToString:self.roomDataSource.roomId]
        && [event.sentError.domain isEqualToString:MXEncryptingErrorDomain]
        && event.sentError.code == MXEncryptingErrorUnknownDeviceCode
        && !unknownDevices)   // Show the alert once in case of resending several events
    {
        __weak __typeof(self) weakSelf = self;
        
        [self dismissTemporarySubViews];
        
        // List all unknown devices
        unknownDevices  = [[MXUsersDevicesMap alloc] init];
        
        NSArray<MXEvent*> *outgoingMsgs = self.roomDataSource.room.outgoingMessages;
        for (MXEvent *event in outgoingMsgs)
        {
            if (event.sentState == MXEventSentStateFailed
                && [event.sentError.domain isEqualToString:MXEncryptingErrorDomain]
                && event.sentError.code == MXEncryptingErrorUnknownDeviceCode)
            {
                MXUsersDevicesMap<MXDeviceInfo*> *eventUnknownDevices = event.sentError.userInfo[MXEncryptingErrorUnknownDeviceDevicesKey];
                
                [unknownDevices addEntriesFromMap:eventUnknownDevices];
            }
        }
        
        UIAlertController *unknownDevicesAlert = [UIAlertController alertControllerWithTitle:[VectorL10n unknownDevicesAlertTitle]
                                                                                     message:[VectorL10n unknownDevicesAlert]
                                                                              preferredStyle:UIAlertControllerStyleAlert];
        
        [unknownDevicesAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n unknownDevicesVerify]
                                                                style:UIAlertActionStyleDefault
                                                              handler:^(UIAlertAction * action) {
            
            if (weakSelf)
            {
                typeof(self) self = weakSelf;
                self->currentAlert = nil;
                
                [self performSegueWithIdentifier:@"showUnknownDevices" sender:self];
            }
            
        }]];
        
        [unknownDevicesAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n unknownDevicesSendAnyway]
                                                                style:UIAlertActionStyleDefault
                                                              handler:^(UIAlertAction * action) {
            
            if (weakSelf)
            {
                typeof(self) self = weakSelf;
                self->currentAlert = nil;
                
                // Acknowledge the existence of all devices
                self->unknownDevices = nil;
                
                // And resend pending messages
                [self resendAllUnsentMessages];
            }
            
        }]];
        
        [unknownDevicesAlert mxk_setAccessibilityIdentifier:@"RoomVCUnknownDevicesAlert"];
        [self presentViewController:unknownDevicesAlert animated:YES completion:nil];
        currentAlert = unknownDevicesAlert;
    }
}

- (void)eventDidChangeIdentifier:(NSNotification *)notif
{
    MXEvent *event = notif.object;
    NSString *previousId = notif.userInfo[kMXEventIdentifierKey];
    
    if ([self.customizedRoomDataSource.selectedEventId isEqualToString:previousId])
    {
        MXLogDebug(@"[RoomVC] eventDidChangeIdentifier: Update selectedEventId");
        self.customizedRoomDataSource.selectedEventId = event.eventId;
    }
}


- (void)resendAllUnsentMessages
{
    // List unsent event ids
    NSArray *outgoingMsgs = self.roomDataSource.room.outgoingMessages;
    NSMutableArray *failedEventIds = [NSMutableArray arrayWithCapacity:outgoingMsgs.count];
    
    for (MXEvent *event in outgoingMsgs)
    {
        if (event.sentState == MXEventSentStateFailed)
        {
            [failedEventIds addObject:event.eventId];
        }
    }
    
    // Launch iterative operation
    [self resendFailedEvent:0 inArray:failedEventIds];
}

- (void)resendFailedEvent:(NSUInteger)index inArray:(NSArray*)failedEventIds
{
    if (index < failedEventIds.count)
    {
        NSString *failedEventId = failedEventIds[index];
        NSUInteger nextIndex = index + 1;
        
        // Let the datasource resend. It will manage local echo, etc.
        [self.roomDataSource resendEventWithEventId:failedEventId success:^(NSString *eventId) {
            
            [self resendFailedEvent:nextIndex inArray:failedEventIds];
            
        } failure:^(NSError *error) {
            
            [self resendFailedEvent:nextIndex inArray:failedEventIds];
            
        }];
        
        return;
    }
    
    // Refresh activities view
    [self refreshActivitiesViewDisplay];
}

- (void)cancelAllUnsentMessages
{
    UIAlertController *cancelAlert = [UIAlertController alertControllerWithTitle:[VectorL10n roomUnsentMessagesCancelTitle]
                                                                         message:[VectorL10n roomUnsentMessagesCancelMessage]
                                                                  preferredStyle:UIAlertControllerStyleAlert];
    
    MXWeakify(self);
    [cancelAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) {
        MXStrongifyAndReturnIfNil(self);
        self->currentAlert = nil;
    }]];
    
    [cancelAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n delete] style:UIAlertActionStyleDestructive handler:^(UIAlertAction * action) {
        MXStrongifyAndReturnIfNil(self);
        // Remove unsent event ids
        for (NSUInteger index = 0; index < self.roomDataSource.room.outgoingMessages.count;)
        {
            MXEvent *event = self.roomDataSource.room.outgoingMessages[index];
            if (event.sentState == MXEventSentStateFailed)
            {
                [self.roomDataSource removeEventWithEventId:event.eventId];
            }
            else
            {
                index ++;
            }
        }
        
        [self refreshActivitiesViewDisplay];
        self->currentAlert = nil;
    }]];
    
    [self presentViewController:cancelAlert animated:YES completion:nil];
    currentAlert = cancelAlert;
}

# pragma mark - Encryption Information view

- (void)showEncryptionInformation:(MXEvent *)event
{
    [self dismissKeyboard];
    
    // Remove potential existing subviews
    [self dismissTemporarySubViews];
    
    EncryptionInfoView *encryptionInfoView = [[EncryptionInfoView alloc] initWithEvent:event andMatrixSession:self.roomDataSource.mxSession];
    
    // Add shadow on added view
    encryptionInfoView.layer.cornerRadius = 5;
    encryptionInfoView.layer.shadowOffset = CGSizeMake(0, 1);
    encryptionInfoView.layer.shadowOpacity = 0.5f;
    
    // Add the view and define edge constraints
    [self.view addSubview:encryptionInfoView];
    
    self->encryptionInfoView = encryptionInfoView;
    
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wdeprecated"
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:encryptionInfoView
                                                          attribute:NSLayoutAttributeTop
                                                          relatedBy:NSLayoutRelationEqual
                                                             toItem:self.topLayoutGuide
                                                          attribute:NSLayoutAttributeBottom
                                                         multiplier:1.0f
                                                           constant:10.0f]];
    
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:encryptionInfoView
                                                          attribute:NSLayoutAttributeBottom
                                                          relatedBy:NSLayoutRelationEqual
                                                             toItem:self.bottomLayoutGuide
                                                          attribute:NSLayoutAttributeTop
                                                         multiplier:1.0f
                                                           constant:-10.0f]];
    #pragma clang diagnostic pop
    
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.view
                                                          attribute:NSLayoutAttributeLeading
                                                          relatedBy:NSLayoutRelationEqual
                                                             toItem:encryptionInfoView
                                                          attribute:NSLayoutAttributeLeading
                                                         multiplier:1.0f
                                                           constant:-10.0f]];
    
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.view
                                                          attribute:NSLayoutAttributeTrailing
                                                          relatedBy:NSLayoutRelationEqual
                                                             toItem:encryptionInfoView
                                                          attribute:NSLayoutAttributeTrailing
                                                         multiplier:1.0f
                                                           constant:10.0f]];
    [self.view setNeedsUpdateConstraints];
}



#pragma mark - Read marker handling

- (void)checkReadMarkerVisibility
{
    if (readMarkerTableViewCell && isAppeared && !self.isBubbleTableViewDisplayInTransition)
    {
        // Check whether the read marker is visible
        CGFloat contentTopPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.adjustedContentInset.top;
        CGFloat readMarkerViewPosY = readMarkerTableViewCell.frame.origin.y + readMarkerTableViewCell.readMarkerView.frame.origin.y;
        if (contentTopPosY <= readMarkerViewPosY)
        {
            // Compute the max vertical position visible according to contentOffset
            CGFloat contentBottomPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.frame.size.height - self.bubblesTableView.adjustedContentInset.bottom;
            if (readMarkerViewPosY <= contentBottomPosY)
            {
                // Launch animation
                [self animateReadMarkerView];
                
                // Disable the read marker display when it has been rendered once.
                self.roomDataSource.showReadMarker = NO;
                [self refreshJumpToLastUnreadBannerDisplay];
                
                // Update the read marker position according the events acknowledgement in this view controller.
                self.updateRoomReadMarker = YES;
                
                if (self.roomDataSource.isLive)
                {
                    // Move the read marker to the current read receipt position.
                    [self.roomDataSource.room forgetReadMarker];
                }
            }
        }
    }
}

- (void)animateReadMarkerView
{
    // Check whether the cell with the read marker is known and if the marker is not animated yet.
    
    if (!readMarkerTableViewCell || readMarkerTableViewCell.readMarkerView.isHidden == NO)
    {
        return;
    }
        
    RoomBubbleCellData *cellData = (RoomBubbleCellData*)readMarkerTableViewCell.bubbleData;
    
    id<RoomTimelineCellDecorator> cellDecorator = [RoomTimelineConfiguration shared].currentStyle.cellDecorator;
    
    [cellDecorator dissmissReadMarkerViewForCell:readMarkerTableViewCell
                                        cellData:cellData
                                        animated:YES
                                      completion:^{
       
        self->readMarkerTableViewCell = nil;
    }];
}

- (void)refreshRemoveJitsiWidgetView
{
    if (!self.displayConfiguration.jitsiWidgetRemoverEnabled)
    {
        return;
    }
    
    if (self.roomDataSource.isLive && !self.roomDataSource.isPeeking)
    {
        Widget *jitsiWidget = [self.customizedRoomDataSource jitsiWidget];
        
        if (jitsiWidget && self.canEditJitsiWidget)
        {
            [self.removeJitsiWidgetView reset];
            self.removeJitsiWidgetContainer.hidden = NO;
            self.removeJitsiWidgetView.delegate = self;
        }
        else
        {
            self.removeJitsiWidgetContainer.hidden = YES;
            self.removeJitsiWidgetView.delegate = nil;
        }
    }
    else
    {
        [self.removeJitsiWidgetView reset];
        self.removeJitsiWidgetContainer.hidden = YES;
        self.removeJitsiWidgetView.delegate = self;
    }
}

- (void)refreshJumpToLastUnreadBannerDisplay
{
    // This banner is only displayed when the room timeline is in live (and no peeking).
    // Check whether the read marker exists and has not been rendered yet.
    if (self.roomDataSource.isLive && !self.roomDataSource.isPeeking && self.roomDataSource.showReadMarker && self.roomDataSource.room.accountData.readMarkerEventId)
    {
        NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
            return [evaluatedObject isKindOfClass:MXKRoomBubbleTableViewCell.class];
        }];
        NSArray *visibleCells = [[self.bubblesTableView visibleCells] filteredArrayUsingPredicate:predicate];
        UITableViewCell *cell = visibleCells.firstObject;
        if (cell)
        {
            MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell*)cell;
            // Check whether the read marker is inside the first displayed cell.
            if (roomBubbleTableViewCell.readMarkerView)
            {
                // The read marker display is still enabled (see roomDataSource.showReadMarker flag),
                // this means the read marker was not been visible yet.
                // We show the banner if the marker is located in the top hidden part of the cell.
                CGFloat contentTopPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.adjustedContentInset.top;
                CGFloat readMarkerViewPosY = roomBubbleTableViewCell.frame.origin.y + roomBubbleTableViewCell.readMarkerView.frame.origin.y;
                self.jumpToLastUnreadBannerContainer.hidden = (contentTopPosY < readMarkerViewPosY);
            }
            else
            {
                // Check whether the read marker event is anterior to the first event displayed in the first rendered cell.
                MXKRoomBubbleComponent *component = roomBubbleTableViewCell.bubbleData.bubbleComponents.firstObject;
                MXEvent *firstDisplayedEvent = component.event;
                MXEvent *currentReadMarkerEvent = [self.roomDataSource.mxSession.store eventWithEventId:self.roomDataSource.room.accountData.readMarkerEventId inRoom:self.roomDataSource.roomId];
                
                if (!currentReadMarkerEvent || (currentReadMarkerEvent.originServerTs < firstDisplayedEvent.originServerTs))
                {
                    self.jumpToLastUnreadBannerContainer.hidden = NO;
                }
                else
                {
                    self.jumpToLastUnreadBannerContainer.hidden = YES;
                    
                    // Force the read marker position in order to not depend on the read marker animation (https://github.com/vector-im/element-ios/issues/7420)
                    self.updateRoomReadMarker = YES;
                }
            }
        }
    }
    else
    {
        self.jumpToLastUnreadBannerContainer.hidden = YES;
        
        // Initialize the read marker if it does not exist yet, only in case of live timeline.
        if (!self.roomDataSource.room.accountData.readMarkerEventId && self.roomDataSource.isLive && !self.roomDataSource.isPeeking)
        {
            // Move the read marker to the current read receipt position by default.
            [self.roomDataSource.room forgetReadMarker];
        }
    }
}

#pragma mark - ContactsTableViewControllerDelegate

- (void)contactsTableViewController:(ContactsTableViewController *)contactsTableViewController didSelectContact:(MXKContact*)contact
{
    __weak typeof(self) weakSelf = self;
    
    if (currentAlert)
    {
        [currentAlert dismissViewControllerAnimated:NO completion:nil];
        currentAlert = nil;
    }
    
    // Invite ?
    NSString *promptMsg = [VectorL10n roomParticipantsInvitePromptMsg:contact.displayName];
    UIAlertController *invitePrompt = [UIAlertController alertControllerWithTitle:[VectorL10n roomParticipantsInvitePromptTitle]
                                                                         message:promptMsg
                                                                  preferredStyle:UIAlertControllerStyleAlert];
    
    [invitePrompt addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
                                                     style:UIAlertActionStyleCancel
                                                   handler:^(UIAlertAction * action) {
        
        if (weakSelf)
        {
            typeof(self) self = weakSelf;
            self->currentAlert = nil;
        }
        
    }]];
    
    [invitePrompt addAction:[UIAlertAction actionWithTitle:[VectorL10n invite]
                                                     style:UIAlertActionStyleDefault
                                                   handler:^(UIAlertAction * action) {
        
        // Sanity check
        if (!weakSelf)
        {
            return;
        }
        
        typeof(self) self = weakSelf;
        self->currentAlert = nil;
        
        MXSession* session = self.roomDataSource.mxSession;
        NSString* roomId = self.roomDataSource.roomId;
        MXRoom *room = [session roomWithRoomId:roomId];
        
        NSArray *identifiers = contact.matrixIdentifiers;
        NSString *participantId;
        
        if (identifiers.count)
        {
            participantId = identifiers.firstObject;
            
            // Invite this user if a room is defined
            [room inviteUser:participantId success:^{
                
                // Refresh display by removing the contacts picker
                [contactsTableViewController withdrawViewControllerAnimated:YES completion:nil];
                
            } failure:^(NSError *error) {
                
                MXLogDebug(@"[RoomVC] Invite %@ failed", participantId);
                // Alert user
                [self showError:error];
                
            }];
        }
        else
        {
            if (contact.emailAddresses.count)
            {
                // This is a local contact, consider the first email by default.
                // TODO: Prompt the user to select the right email.
                MXKEmail *email = contact.emailAddresses.firstObject;
                participantId = email.emailAddress;
            }
            else
            {
                // This is the text filled by the user.
                participantId = contact.displayName;
            }
            
            // Is it an email or a Matrix user ID?
            if ([MXTools isEmailAddress:participantId])
            {
                [room inviteUserByEmail:participantId success:^{
                    
                    // Refresh display by removing the contacts picker
                    [contactsTableViewController withdrawViewControllerAnimated:YES completion:nil];
                    
                } failure:^(NSError *error) {
                    
                    MXLogDebug(@"[RoomVC] Invite be email %@ failed", participantId);
                    // Alert user
                    if ([error.domain isEqualToString:kMXRestClientErrorDomain]
                        && error.code == MXRestClientErrorMissingIdentityServer)
                    {
                        [self showAlertWithTitle:[VectorL10n errorInvite3pidWithNoIdentityServer] message:nil];
                    }
                    else
                    {
                        [self showError:error];
                    }
                }];
            }
            else //if ([MXTools isMatrixUserIdentifier:participantId])
            {
                [room inviteUser:participantId success:^{
                    
                    // Refresh display by removing the contacts picker
                    [contactsTableViewController withdrawViewControllerAnimated:YES completion:nil];
                    
                } failure:^(NSError *error) {
                    
                    MXLogDebug(@"[RoomVC] Invite %@ failed", participantId);
                    // Alert user
                    [self showError:error];
                    
                }];
            }
        }
        
    }]];
    
    [invitePrompt mxk_setAccessibilityIdentifier:@"RoomVCInviteAlert"];
    [self presentViewController:invitePrompt animated:YES completion:nil];
    currentAlert = invitePrompt;
}

#pragma mark - Re-request encryption keys

- (void)reRequestKeysAndShowExplanationAlert:(MXEvent*)event
{
    MXWeakify(self);
    __block UIAlertController *alert;
    
    // Force device verification if session has cross-signing activated and device is not yet verified
    if (self.mainSession.crypto.crossSigning && self.mainSession.crypto.crossSigning.state == MXCrossSigningStateCrossSigningExists)
    {
        [self presentReviewUnverifiedSessionsAlert];
        return;
    }
    
    // Make the re-request
    [self.mainSession.crypto reRequestRoomKeyForEvent:event];
    
    // Observe kMXEventDidDecryptNotification to remove automatically the dialog
    // if the user has shared the keys from another device
    mxEventDidDecryptNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXEventDidDecryptNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
        MXStrongifyAndReturnIfNil(self);
        
        MXEvent *decryptedEvent = notif.object;
        
        if ([decryptedEvent.eventId isEqualToString:event.eventId])
        {
            [[NSNotificationCenter defaultCenter] removeObserver:self->mxEventDidDecryptNotificationObserver];
            self->mxEventDidDecryptNotificationObserver = nil;
            
            if (self->currentAlert == alert)
            {
                [self->currentAlert dismissViewControllerAnimated:YES completion:nil];
                self->currentAlert = nil;
            }
        }
    }];
    
    // Show the explanation dialog
    alert = [UIAlertController alertControllerWithTitle:VectorL10n.rerequestKeysAlertTitle
                                                message:[VectorL10n e2eRoomKeyRequestMessage:AppInfo.current.displayName]
                                         preferredStyle:UIAlertControllerStyleAlert];
    
    [alert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok]
                                              style:UIAlertActionStyleDefault
                                            handler:^(UIAlertAction * action)
                      {
        MXStrongifyAndReturnIfNil(self);
        
        [[NSNotificationCenter defaultCenter] removeObserver:self->mxEventDidDecryptNotificationObserver];
        self->mxEventDidDecryptNotificationObserver = nil;
        
        self->currentAlert = nil;
    }]];
    
    [self presentViewController:alert animated:YES completion:nil];
    currentAlert = alert;
}

- (void)presentReviewUnverifiedSessionsAlert
{
    MXLogDebug(@"[MasterTabBarController] presentReviewUnverifiedSessionsAlertWithSession");
    
    [currentAlert dismissViewControllerAnimated:NO completion:nil];
    
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:[VectorL10n keyVerificationAlertTitle]
                                                                   message:[VectorL10n keyVerificationAlertBody]
                                                            preferredStyle:UIAlertControllerStyleAlert];
    
    [alert addAction:[UIAlertAction actionWithTitle:[VectorL10n keyVerificationSelfVerifyUnverifiedSessionsAlertValidateAction]
                                              style:UIAlertActionStyleDefault
                                            handler:^(UIAlertAction * action) {
        [self showSettingsSecurityScreen];
    }]];
    
    [alert addAction:[UIAlertAction actionWithTitle:[VectorL10n later]
                                              style:UIAlertActionStyleCancel
                                            handler:nil]];
    
    [self presentViewController:alert animated:YES completion:nil];
    
    currentAlert = alert;
}

- (void)showSettingsSecurityScreen
{
    if (self.delegate)
    {
        [self.delegate roomViewController:self showCompleteSecurityForSession:self.mainSession];
    }
    else
    {
        [[AppDelegate theDelegate] presentCompleteSecurityForSession: self.mainSession];
    }
}

#pragma mark Tombstone event

- (void)listenTombstoneEventNotifications
{
    // Room is already obsolete do not listen to tombstone event
    if (self.roomDataSource.roomState.isObsolete)
    {
        return;
    }
    
    MXWeakify(self);
    
    tombstoneEventNotificationsListener = [self.roomDataSource.room listenToEventsOfTypes:@[kMXEventTypeStringRoomTombStone] onEvent:^(MXEvent *event, MXTimelineDirection direction, MXRoomState *roomState) {
        
        MXStrongifyAndReturnIfNil(self);
        
        // Update activitiesView with room replacement information
        [self refreshActivitiesViewDisplay];
        // Hide inputToolbarView
        [self updateRoomInputToolbarViewClassIfNeeded];
    }];
}

- (void)removeTombstoneEventNotificationsListener
{
    if (self.roomDataSource)
    {
        // Remove the previous live listener
        if (tombstoneEventNotificationsListener)
        {
            [self.roomDataSource.room removeListener:tombstoneEventNotificationsListener];
            tombstoneEventNotificationsListener = nil;
        }
    }
}

#pragma mark MXSession state change

- (void)listenMXSessionStateChangeNotifications
{
    MXWeakify(self);
    
    kMXSessionStateDidChangeObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXSessionStateDidChangeNotification object:self.roomDataSource.mxSession queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
        
        MXStrongifyAndReturnIfNil(self);
        
        if (self.roomDataSource.mxSession.state == MXSessionStateSyncError
            || self.roomDataSource.mxSession.state == MXSessionStateRunning)
        {
            [self refreshActivitiesViewDisplay];
            
            // update inputToolbarView
            [self updateRoomInputToolbarViewClassIfNeeded];
        }
    }];
}

- (void)removeMXSessionStateChangeNotificationsListener
{
    if (kMXSessionStateDidChangeObserver)
    {
        [[NSNotificationCenter defaultCenter] removeObserver:kMXSessionStateDidChangeObserver];
        kMXSessionStateDidChangeObserver = nil;
    }
}

#pragma mark - Contextual Menu

- (NSArray<RoomContextualMenuItem*>*)contextualMenuItemsForEvent:(MXEvent*)event andCell:(id<MXKCellRendering>)cell
{
    if (event.sentState == MXEventSentStateFailed)
    {
        return @[
            [self resendMenuItemWithEvent:event],
            [self deleteMenuItemWithEvent:event],
            [self editMenuItemWithEvent:event],
            [self copyMenuItemWithEvent:event andCell:cell]
        ];
    }
    
    BOOL showMoreOption = (event.isState && RiotSettings.shared.roomContextualMenuShowMoreOptionForStates)
        || (!event.isState && RiotSettings.shared.roomContextualMenuShowMoreOptionForMessages);
    BOOL showThreadOption = [self showThreadOptionForEvent:event];
    
    NSMutableArray<RoomContextualMenuItem*> *items = [NSMutableArray arrayWithCapacity:5];
    
    [items addObject:[self replyMenuItemWithEvent:event]];
    if (showThreadOption)
    {
        //  add "Thread" option only if not already in a thread
        [items addObject:[self replyInThreadMenuItemWithEvent:event]];
    }
    [items addObject:[self editMenuItemWithEvent:event]];
    if (!showThreadOption)
    {
        [items addObject:[self copyMenuItemWithEvent:event andCell:cell]];
    }
    if (showMoreOption)
    {
        [items addObject:[self moreMenuItemWithEvent:event andCell:cell]];
    }
    
    return items;
}

- (void)showContextualMenuForEvent:(MXEvent*)event fromSingleTapGesture:(BOOL)usedSingleTapGesture cell:(id<MXKCellRendering>)cell animated:(BOOL)animated
{
    if (self.roomContextualMenuPresenter.isPresenting)
    {
        return;
    }
    
    NSString *selectedEventId = event.eventId;
    
    NSArray<RoomContextualMenuItem*>* contextualMenuItems = [self contextualMenuItemsForEvent:event andCell:cell];
    ReactionsMenuViewModel *reactionsMenuViewModel;
    CGRect bubbleComponentFrameInOverlayView = CGRectNull;
    
    if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && [self.roomDataSource canReactToEventWithId:event.eventId])
    {
        MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell*)cell;
        MXKRoomBubbleCellData *bubbleCellData = roomBubbleTableViewCell.bubbleData;
        NSArray *bubbleComponents = bubbleCellData.bubbleComponents;
        
        NSInteger foundComponentIndex = [bubbleCellData bubbleComponentIndexForEventId:event.eventId];
        CGRect bubbleComponentFrame;
        
        if (bubbleComponents.count > 0)
        {
            NSInteger selectedComponentIndex = foundComponentIndex != NSNotFound ? foundComponentIndex : 0;
            bubbleComponentFrame = [roomBubbleTableViewCell surroundingFrameInTableViewForComponentIndex:selectedComponentIndex];
        }
        else
        {
            bubbleComponentFrame = roomBubbleTableViewCell.frame;
        }
        
        bubbleComponentFrameInOverlayView = [self.bubblesTableView convertRect:bubbleComponentFrame toView:self.overlayContainerView];
        
        NSString *roomId = self.roomDataSource.roomId;
        MXAggregations *aggregations = self.mainSession.aggregations;
        MXAggregatedReactions *aggregatedReactions = [aggregations aggregatedReactionsOnEvent:selectedEventId inRoom:roomId];
        
        reactionsMenuViewModel = [[ReactionsMenuViewModel alloc] initWithAggregatedReactions:aggregatedReactions eventId:selectedEventId];
        reactionsMenuViewModel.coordinatorDelegate = self;
    }
    
    if (!self.roomContextualMenuViewController)
    {
        self.roomContextualMenuViewController = [RoomContextualMenuViewController instantiate];
        self.roomContextualMenuViewController.delegate = self;
    }
    
    [self.roomContextualMenuViewController updateWithContextualMenuItems:contextualMenuItems reactionsMenuViewModel:reactionsMenuViewModel];
    
    [self enableOverlayContainerUserInteractions:YES];
    
    [self.roomContextualMenuPresenter presentWithRoomContextualMenuViewController:self.roomContextualMenuViewController
                                                                             from:self
                                                                               on:self.overlayContainerView
                                                              contentToReactFrame:bubbleComponentFrameInOverlayView
                                                             fromSingleTapGesture:usedSingleTapGesture
                                                                         animated:animated
                                                                       completion:^{
    }];
    
    preventBubblesTableViewScroll = YES;
    [self selectEventWithId:selectedEventId];
}

- (void)hideContextualMenuAnimated:(BOOL)animated
{
    [self hideContextualMenuAnimated:animated completion:nil];
}

- (void)hideContextualMenuAnimated:(BOOL)animated completion:(void(^)(void))completion
{
    [self hideContextualMenuAnimated:animated cancelEventSelection:YES completion:completion];
}

- (void)hideContextualMenuAnimated:(BOOL)animated cancelEventSelection:(BOOL)cancelEventSelection completion:(void(^)(void))completion
{
    if (!self.roomContextualMenuPresenter.isPresenting)
    {
        return;
    }
    
    if (cancelEventSelection)
    {
        [self cancelEventSelection];
    }
    
    preventBubblesTableViewScroll = NO;
    
    [self.roomContextualMenuPresenter hideContextualMenuWithAnimated:animated completion:^{
        [self enableOverlayContainerUserInteractions:NO];
        
        if (completion)
        {
            completion();
        }
    }];
}

- (void)enableOverlayContainerUserInteractions:(BOOL)enableOverlayContainerUserInteractions
{
    self.inputToolbarView.editable = !enableOverlayContainerUserInteractions;
    self.bubblesTableView.scrollsToTop = !enableOverlayContainerUserInteractions;
    self.overlayContainerView.userInteractionEnabled = enableOverlayContainerUserInteractions;
}

- (RoomContextualMenuItem *)resendMenuItemWithEvent:(MXEvent*)event
{
    MXWeakify(self);
    
    RoomContextualMenuItem *resendMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionResend];
    resendMenuItem.action = ^{
        MXStrongifyAndReturnIfNil(self);
        [self hideContextualMenuAnimated:YES cancelEventSelection:NO completion:nil];
        [self cancelEventSelection];
        [self.roomDataSource resendEventWithEventId:event.eventId success:nil failure:nil];
    };
    
    return resendMenuItem;
}

- (RoomContextualMenuItem *)deleteMenuItemWithEvent:(MXEvent*)event
{
    MXWeakify(self);
    
    RoomContextualMenuItem *deleteMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionDelete];
    deleteMenuItem.action = ^{
        MXStrongifyAndReturnIfNil(self);
        
        MXWeakify(self);
        [self hideContextualMenuAnimated:YES cancelEventSelection:YES completion:^{
            MXStrongifyAndReturnIfNil(self);
            
            UIAlertController *deleteConfirmation = [UIAlertController alertControllerWithTitle:[VectorL10n roomEventActionDeleteConfirmationTitle]
                                                                                        message:[VectorL10n roomEventActionDeleteConfirmationMessage]
                                                                                 preferredStyle:UIAlertControllerStyleAlert];
            
            [deleteConfirmation addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
            }]];
            
            [deleteConfirmation addAction:[UIAlertAction actionWithTitle:[VectorL10n delete] style:UIAlertActionStyleDestructive handler:^(UIAlertAction * action) {
                [self.roomDataSource removeEventWithEventId:event.eventId];
            }]];
            
            [self presentViewController:deleteConfirmation animated:YES completion:nil];
            self->currentAlert = deleteConfirmation;
        }];
    };
    
    return deleteMenuItem;
}

- (RoomContextualMenuItem *)editMenuItemWithEvent:(MXEvent*)event
{
    MXWeakify(self);
    
    RoomContextualMenuItem *editMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionEdit];
    
    switch (event.eventType) {
        case MXEventTypePollStart: {
            editMenuItem.action = ^{
                MXStrongifyAndReturnIfNil(self);
                [self hideContextualMenuAnimated:YES cancelEventSelection:YES completion:nil];
                [self.delegate roomViewController:self didRequestEditForPollWithStartEvent:event];
            };
            
            editMenuItem.isEnabled = [self.delegate roomViewController:self canEditPollWithEventIdentifier:event.eventId];
            
            break;
        }
        default: {
            editMenuItem.action = ^{
                MXStrongifyAndReturnIfNil(self);
                [self hideContextualMenuAnimated:YES cancelEventSelection:NO completion:nil];
                [self editEventContentWithId:event.eventId];
                
                // And display the keyboard
                [self.inputToolbarView becomeFirstResponder];
            };
            
            editMenuItem.isEnabled = [self.roomDataSource canEditEventWithId:event.eventId];
            
            break;
        }
    }
    
    return editMenuItem;
}

- (RoomContextualMenuItem *)copyMenuItemWithEvent:(MXEvent*)event andCell:(id<MXKCellRendering>)cell
{
    MXWeakify(self);
    
    RoomContextualMenuItem *copyMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionCopy];
    copyMenuItem.isEnabled = [self canCopyEvent:event andCell:cell];
    MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell;
    MXKRoomBubbleCellData *cellData = roomBubbleTableViewCell.bubbleData;
    copyMenuItem.action = ^{
        MXStrongifyAndReturnIfNil(self);
        
        [self copyEvent:event inCell:cell withCellData:cellData];
    };
    
    return copyMenuItem;
}

- (BOOL)canCopyEvent:(MXEvent*)event andCell:(id<MXKCellRendering>)cell
{
    MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell;
    MXKAttachment *attachment = roomBubbleTableViewCell.bubbleData.attachment;
    
    BOOL result = !attachment || attachment.type != MXKAttachmentTypeSticker;
    
    if (attachment && !BuildSettings.messageDetailsAllowCopyMedia)
    {
        result = NO;
    }
    
    if (result)
    {
        switch (event.eventType) {
            case MXEventTypeRoomMessage:
            {
                NSString *messageType = event.content[kMXMessageTypeKey];
                
                if ([messageType isEqualToString:kMXMessageTypeKeyVerificationRequest])
                {
                    result = NO;
                }
                break;
            }
            case MXEventTypeKeyVerificationStart:
            case MXEventTypeKeyVerificationAccept:
            case MXEventTypeKeyVerificationKey:
            case MXEventTypeKeyVerificationMac:
            case MXEventTypeKeyVerificationDone:
            case MXEventTypeKeyVerificationCancel:
            case MXEventTypePollStart:
            case MXEventTypePollEnd:
            case MXEventTypeBeaconInfo:
                result = NO;
                break;
            case MXEventTypeCustom:
                if ([event.type isEqualToString:kWidgetMatrixEventTypeString]
                    || [event.type isEqualToString:kWidgetModularEventTypeString])
                {
                    Widget *widget = [[Widget alloc] initWithWidgetEvent:event inMatrixSession:self.roomDataSource.mxSession];
                    if ([widget.type isEqualToString:kWidgetTypeJitsiV1] ||
                        [widget.type isEqualToString:kWidgetTypeJitsiV2])
                    {
                        result = NO;
                    }
                }
            default:
                break;
        }
    }
    
    return result;
}

- (void)copyEvent:(MXEvent*)event inCell:(id<MXKCellRendering>)cell withCellData:(MXKRoomBubbleCellData *)cellData
{
    MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell;
    MXKAttachment *attachment = cellData.attachment;
    
    if (!attachment)
    {
        NSArray *components = cellData.bubbleComponents;
        MXKRoomBubbleComponent *selectedComponent;
        for (selectedComponent in components)
        {
            if ([selectedComponent.event.eventId isEqualToString:event.eventId])
            {
                break;
            }
            selectedComponent = nil;
        }

        NSAttributedString *attributedTextMessage = selectedComponent.attributedTextMessage;
        
        if (attributedTextMessage)
        {
            if (@available(iOS 15.0, *))
            {
                MXKPasteboardManager.shared.pasteboard.string = [PillsFormatter stringByReplacingPillsIn:attributedTextMessage
                                                                                                    mode:PillsReplacementTextModeMarkdown];
            }
            else
            {
                MXKPasteboardManager.shared.pasteboard.string = attributedTextMessage.string;
            }
        }
        else
        {
            MXLogDebug(@"[RoomViewController] Contextual menu copy failed. Text is nil for room id/event id: %@/%@", selectedComponent.event.roomId, selectedComponent.event.eventId);
        }
        
        [self hideContextualMenuAnimated:YES];
    }
    else if (attachment.type != MXKAttachmentTypeSticker)
    {
        [self hideContextualMenuAnimated:YES completion:^{
            [self startActivityIndicator];
            
            [attachment copy:^{
                
                [self stopActivityIndicator];
                
            } failure:^(NSError *error) {
                
                [self stopActivityIndicator];
                
                //Alert user
                [self showError:error];
            }];
            
            // Start animation in case of download during attachment preparing
            [roomBubbleTableViewCell startProgressUI];
        }];
    }
}

- (RoomContextualMenuItem *)replyMenuItemWithEvent:(MXEvent*)event
{
    MXWeakify(self);
    
    RoomContextualMenuItem *replyMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionReply];
    replyMenuItem.isEnabled = [self.roomDataSource canReplyToEventWithId:event.eventId] && !self.voiceMessageController.isRecordingAudio;
    replyMenuItem.action = ^{
        MXStrongifyAndReturnIfNil(self);
        
        [self hideContextualMenuAnimated:YES cancelEventSelection:NO completion:nil];
        [self selectEventWithId:event.eventId inputToolBarSendMode:RoomInputToolbarViewSendModeReply showTimestamp:NO];
        
        // And display the keyboard
        [self.inputToolbarView becomeFirstResponder];
    };
    
    return replyMenuItem;
}

- (RoomContextualMenuItem *)replyInThreadMenuItemWithEvent:(MXEvent*)event
{
    MXWeakify(self);
    
    RoomContextualMenuItem *item = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionReplyInThread];
    item.isEnabled = [self.roomDataSource canReplyToEventWithId:event.eventId] && !self.voiceMessageController.isRecordingAudio;
    item.action = ^{
        MXStrongifyAndReturnIfNil(self);
        
        [self hideContextualMenuAnimated:YES cancelEventSelection:NO completion:nil];

        if (RiotSettings.shared.enableThreads)
        {
            [self openThreadWithId:event.eventId];
        }
        else
        {
            [self showThreadsBetaForEvent:event];
        }
    };
    
    return item;
}

- (RoomContextualMenuItem *)moreMenuItemWithEvent:(MXEvent*)event andCell:(id<MXKCellRendering>)cell
{
    MXWeakify(self);
    
    RoomContextualMenuItem *moreMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionMore];
    moreMenuItem.action = ^{
        MXStrongifyAndReturnIfNil(self);
        [self hideContextualMenuAnimated:YES completion:nil];
        [self showAdditionalActionsMenuForEvent:event inCell:cell animated:YES];
    };
    
    return moreMenuItem;
}

#pragma mark - Threads

- (BOOL)showThreadOptionForEvent:(MXEvent*)event
{
    return !self.roomDataSource.threadId
        && !event.threadId
        && (RiotSettings.shared.enableThreads || self.mainSession.store.supportedMatrixVersions.supportsThreads);
}

- (void)showThreadsNotice
{
    if (!self.threadsNoticeModalPresenter)
    {
        self.threadsNoticeModalPresenter = [SlidingModalPresenter new];
    }

    [self.threadsNoticeModalPresenter dismissWithAnimated:NO completion:nil];

    ThreadsNoticeViewController *threadsNoticeVC = [ThreadsNoticeViewController instantiate];

    MXWeakify(self);

    threadsNoticeVC.didTapDoneButton = ^{

        MXStrongifyAndReturnIfNil(self);

        [self.threadsNoticeModalPresenter dismissWithAnimated:YES completion:^{
            RiotSettings.shared.threadsNoticeDisplayed = YES;
        }];
    };

    [self.threadsNoticeModalPresenter present:threadsNoticeVC
                                         from:self.presentedViewController?:self
                                     animated:YES
                                      options:SlidingModalPresenter.SpanningOption
                                   completion:nil];
}

- (void)showThreadsBetaForEvent:(MXEvent *)event
{
    if (self.threadsBetaBridgePresenter)
    {
        [self.threadsBetaBridgePresenter dismissWithAnimated:YES completion:nil];
        self.threadsBetaBridgePresenter = nil;
    }

    self.threadsBetaBridgePresenter = [[ThreadsBetaCoordinatorBridgePresenter alloc] initWithThreadId:event.eventId
                                                                                             infoText:VectorL10n.threadsBetaInformation
                                                                                       additionalText:nil];
    self.threadsBetaBridgePresenter.delegate = self;

    [self.threadsBetaBridgePresenter presentFrom:self.presentedViewController?:self animated:YES];
}

- (void)openThreadWithId:(NSString *)threadId
{
    if (self.threadsBridgePresenter)
    {
        [self.threadsBridgePresenter dismissWithAnimated:YES completion:nil];
        self.threadsBridgePresenter = nil;
    }

    self.threadsBridgePresenter = [self.delegate threadsCoordinatorForRoomViewController:self threadId:threadId];
    self.threadsBridgePresenter.delegate = self;
    [self.threadsBridgePresenter pushFrom:self.navigationController animated:YES];
}

- (void)highlightAndDisplayEvent:(NSString *)eventId completion:(void (^)(void))completion
{
    NSInteger row = [self.roomDataSource indexOfCellDataWithEventId:eventId];
    if (row == NSNotFound)
    {
        //  event with eventId is not loaded into data source yet, load another data source and display it
        [self startActivityIndicator];
        MXWeakify(self);
        [RoomDataSource loadRoomDataSourceWithRoomId:self.roomDataSource.roomId
                                      initialEventId:eventId
                                            threadId:nil
                                    andMatrixSession:self.roomDataSource.mxSession
                                          onComplete:^(RoomDataSource *roomDataSource) {
            MXStrongifyAndReturnIfNil(self);
            [roomDataSource finalizeInitialization];
            [self stopActivityIndicator];
            roomDataSource.markTimelineInitialEvent = YES;
            [self displayRoom:roomDataSource];
            // Give the data source ownership to the room view controller.
            self.hasRoomDataSourceOwnership = YES;
            if (completion)
            {
                completion();
            }
        }];
        return;
    }
    
    NSMutableArray<NSIndexPath *> *rowsToReload = [[NSMutableArray alloc] init];
    // Get the current hightlighted event because we will need to reload it
    NSString *currentHiglightedEventId = self.customizedRoomDataSource.highlightedEventId;
    if (currentHiglightedEventId)
    {
        NSInteger currentHiglightedRow = [self.roomDataSource indexOfCellDataWithEventId:currentHiglightedEventId];
        if (currentHiglightedRow != NSNotFound)
        {
            NSIndexPath *indexPath = [NSIndexPath indexPathForRow:currentHiglightedRow inSection:0];
            if ([[self.bubblesTableView indexPathsForVisibleRows] containsObject:indexPath])
            {
                [rowsToReload addObject:indexPath];
            }
        }
    }
    
    self.customizedRoomDataSource.highlightedEventId = eventId;
    
    // Add the new highligted event to the list of rows to reload
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0];
    BOOL indexPathIsVisible = [[self.bubblesTableView indexPathsForVisibleRows] containsObject:indexPath];
    if (indexPathIsVisible)
    {
        [rowsToReload addObject:indexPath];
    }
    
    // Reload rows
    if (rowsToReload.count > 0)
    {
        [self.bubblesTableView reloadRowsAtIndexPaths:rowsToReload
                                     withRowAnimation:UITableViewRowAnimationNone];
    }
    
    // Scroll to the newly highlighted row
    if (indexPathIsVisible || [self.bubblesTableView vc_hasIndexPath:indexPath])
    {
        [self.bubblesTableView scrollToRowAtIndexPath:indexPath
                                     atScrollPosition:UITableViewScrollPositionMiddle
                                             animated:YES];
    }

    if (completion)
    {
        completion();
    }
}

- (void)cancelEventHighlight
{
    //  if data source is highlighting an event, dismiss the highlight when user dragges the table view
    if (self.customizedRoomDataSource.highlightedEventId)
    {
        NSInteger row = [self.roomDataSource indexOfCellDataWithEventId:self.customizedRoomDataSource.highlightedEventId];
        if (row == NSNotFound)
        {
            self.customizedRoomDataSource.highlightedEventId = nil;
            return;
        }
        
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0];
        if ([[self.bubblesTableView indexPathsForVisibleRows] containsObject:indexPath])
        {
            self.customizedRoomDataSource.highlightedEventId = nil;
            [self.bubblesTableView reloadRowsAtIndexPaths:@[indexPath]
                                         withRowAnimation:UITableViewRowAnimationAutomatic];
        }
    }
}

- (void)updateThreadListBarButtonBadgeWith:(MXThreadingService *)service
{
    [self updateThreadListBarButtonItem:nil with:service];
}

- (void)updateThreadListBarButtonItem:(UIBarButtonItem *)barButtonItem with:(MXThreadingService *)service
{
    if (!service || _isWaitingForOtherParticipants)
    {
        return;
    }

    __block NSInteger replaceIndex = NSNotFound;
    [self.navigationItem.rightBarButtonItems enumerateObjectsUsingBlock:^(UIBarButtonItem * _Nonnull item, NSUInteger index, BOOL * _Nonnull stop)
     {
        if (item.tag == kThreadListBarButtonItemTag)
        {
            replaceIndex = index;
            *stop = YES;
        }
    }];

    if (!barButtonItem && replaceIndex == NSNotFound)
    {
        //  there is no thread list bar button item, and not provided another to update
        //  ignore
        return;
    }

    UIBarButtonItem *threadListBarButtonItem = barButtonItem ?: [self threadListBarButtonItem];
    UIButton *button = (UIButton *)threadListBarButtonItem.customView;
    
    MXThreadNotificationsCount *notificationsCount = [service notificationsCountForRoom:self.roomDataSource.roomId];
    
    UIImage *buttonIcon = [AssetImages.threadsIcon.image vc_resizedWith:kThreadListBarButtonItemImageSize];
    [button setImage:buttonIcon forState:UIControlStateNormal];
    button.contentEdgeInsets = kThreadListBarButtonItemContentInsetsNoDot;

    if (notificationsCount.notificationsNumber > 0)
    {
        BadgeLabel *badgeLabel = [[BadgeLabel alloc] init];
        badgeLabel.text = notificationsCount.notificationsNumber > 99 ? @"99+" : [NSString stringWithFormat:@"%lu", notificationsCount.notificationsNumber];
        id<Theme> theme = ThemeService.shared.theme;
        badgeLabel.font = theme.fonts.caption1SB;
        badgeLabel.textColor = theme.colors.navigation;
        badgeLabel.badgeColor = notificationsCount.numberOfHighlightedThreads ? theme.colors.alert : theme.colors.secondaryContent;
        [button addSubview:badgeLabel];
        
        [badgeLabel layoutIfNeeded];
        
        badgeLabel.translatesAutoresizingMaskIntoConstraints = NO;
        [badgeLabel.centerYAnchor constraintEqualToAnchor:button.centerYAnchor
                                                constant:badgeLabel.bounds.size.height - buttonIcon.size.height / 2].active = YES;
        [badgeLabel.centerXAnchor constraintEqualToAnchor:button.centerXAnchor
                                                 constant:badgeLabel.bounds.size.width + buttonIcon.size.width / 2].active = YES;
    }

    if (replaceIndex == NSNotFound)
    {
        // there is no thread list bar button item, this was only an update
        return;
    }

    UIBarButtonItem *originalItem = self.navigationItem.rightBarButtonItems[replaceIndex];
    UIButton *originalButton = (UIButton *)originalItem.customView;
    if ([originalButton imageForState:UIControlStateNormal] == [button imageForState:UIControlStateNormal]
        && UIEdgeInsetsEqualToEdgeInsets(originalButton.contentEdgeInsets, button.contentEdgeInsets))
    {
        //  no need to replace, it's the same
        return;
    }
    NSMutableArray<UIBarButtonItem*> *items = [self.navigationItem.rightBarButtonItems mutableCopy];
    items[replaceIndex] = threadListBarButtonItem;
    self.navigationItem.rightBarButtonItems = items;
}

#pragma mark - RoomContextualMenuViewControllerDelegate

- (void)roomContextualMenuViewControllerDidTapBackgroundOverlay:(RoomContextualMenuViewController *)viewController
{
    [self hideContextualMenuAnimated:YES];
}

#pragma mark - ReactionsMenuViewModelCoordinatorDelegate

- (void)reactionsMenuViewModel:(ReactionsMenuViewModel *)viewModel didAddReaction:(NSString *)reaction forEventId:(NSString *)eventId
{
    MXWeakify(self);
    
    [self hideContextualMenuAnimated:YES completion:^{
        
        [self.roomDataSource addReaction:reaction forEventId:eventId success:^{
            
        } failure:^(NSError *error) {
            MXStrongifyAndReturnIfNil(self);
            
            [self.errorPresenter presentErrorFromViewController:self forError:error animated:YES handler:nil];
        }];
    }];
}

- (void)reactionsMenuViewModel:(ReactionsMenuViewModel *)viewModel didRemoveReaction:(NSString *)reaction forEventId:(NSString *)eventId
{
    MXWeakify(self);
    
    [self hideContextualMenuAnimated:YES completion:^{
        
        [self.roomDataSource removeReaction:reaction forEventId:eventId success:^{
            
        } failure:^(NSError *error) {
            MXStrongifyAndReturnIfNil(self);
            
            [self.errorPresenter presentErrorFromViewController:self forError:error animated:YES handler:nil];
        }];
        
    }];
}

- (void)reactionsMenuViewModelDidTapMoreReactions:(ReactionsMenuViewModel *)viewModel forEventId:(NSString *)eventId
{
    [self hideContextualMenuAnimated:YES];

    [self showEmojiPickerForEventId:eventId];
}

#pragma mark -

- (void)showEditHistoryForEventId:(NSString*)eventId animated:(BOOL)animated
{
    MXEvent *event = [self.roomDataSource eventWithEventId:eventId];
    EditHistoryCoordinatorBridgePresenter *presenter = [[EditHistoryCoordinatorBridgePresenter alloc] initWithSession:self.roomDataSource.mxSession event:event];
    
    presenter.delegate = self;
    [presenter presentFrom:self animated:animated];
    
    self.editHistoryPresenter = presenter;
}

#pragma mark - EditHistoryCoordinatorBridgePresenterDelegate

- (void)editHistoryCoordinatorBridgePresenterDelegateDidComplete:(EditHistoryCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
    [coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
    self.editHistoryPresenter = nil;
}

#pragma mark - DocumentPickerPresenterDelegate

- (void)documentPickerPresenterWasCancelled:(MXKDocumentPickerPresenter *)presenter
{
    self.documentPickerPresenter = nil;
}

- (void)documentPickerPresenter:(MXKDocumentPickerPresenter *)presenter didPickDocumentsAt:(NSURL *)url
{
    self.documentPickerPresenter = nil;
    
    MXKUTI *fileUTI = [[MXKUTI alloc] initWithLocalFileURL:url];
    NSString *mimeType = fileUTI.mimeType;
    
    if (fileUTI.isImage)
    {
        NSData *imageData = [[NSData alloc] initWithContentsOfURL:url];
        
        [self sendImage:imageData mimeType:mimeType];
    }
    else if (fileUTI.isVideo)
    {
        [self sendVideo:url];
    }
    else if (fileUTI.isFile)
    {
        [self sendFile:url mimeType:mimeType];
    }
    else
    {
        MXLogDebug(@"[MXKRoomViewController] File upload using MIME type %@ is not supported.", mimeType);
        
        [self showAlertWithTitle:[VectorL10n fileUploadErrorTitle]
                         message:[VectorL10n fileUploadErrorUnsupportedFileTypeMessage]];
    }
}

- (void)sendImage:(NSData *)imageData mimeType:(NSString *)mimeType {
    // Create before sending the message in case of a discussion (direct chat)
    MXWeakify(self);
    [self createDiscussionIfNeeded:^(BOOL readyToSend) {
        MXStrongifyAndReturnIfNil(self);
        if (readyToSend)
        {
            // Let the datasource send it and manage the local echo
            [self.roomDataSource sendImage:imageData mimeType:mimeType success:nil failure:^(NSError *error) {
                // Nothing to do. The image is marked as unsent in the room history by the datasource
                MXLogDebug(@"[MXKRoomViewController] sendImage failed.");
            }];
        }
        // Errors are handled at the request level. This should be improved in case of code rewriting.
    }];
}

- (void)sendVideo:(NSURL * _Nonnull)url {
    // Create before sending the message in case of a discussion (direct chat)
    MXWeakify(self);
    [self createDiscussionIfNeeded:^(BOOL readyToSend) {
        MXStrongifyAndReturnIfNil(self);
        if (readyToSend)
        {
            // Let the datasource send it and manage the local echo
            [(RoomDataSource*)self.roomDataSource sendVideo:url success:nil failure:^(NSError *error) {
                // Nothing to do. The video is marked as unsent in the room history by the datasource
                MXLogDebug(@"[MXKRoomViewController] sendVideo failed.");
            }];
        }
        // Errors are handled at the request level. This should be improved in case of code rewriting.
    }];
}

- (void)sendFile:(NSURL * _Nonnull)url mimeType:(NSString *)mimeType {
    // Create before sending the message in case of a discussion (direct chat)
    MXWeakify(self);
    [self createDiscussionIfNeeded:^(BOOL readyToSend) {
        MXStrongifyAndReturnIfNil(self);
        if (readyToSend)
        {
            // Let the datasource send it and manage the local echo
            [self.roomDataSource sendFile:url mimeType:mimeType success:nil failure:^(NSError *error) {
                // Nothing to do. The file is marked as unsent in the room history by the datasource
                MXLogDebug(@"[MXKRoomViewController] sendFile failed.");
            }];
        }
        // Errors are handled at the request level. This should be improved in case of code rewriting.
    }];
}

#pragma mark - EmojiPickerCoordinatorBridgePresenterDelegate

- (void)emojiPickerCoordinatorBridgePresenter:(EmojiPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter didAddEmoji:(NSString *)emoji forEventId:(NSString *)eventId
{
    MXWeakify(self);
    
    [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{
        [self.roomDataSource addReaction:emoji forEventId:eventId success:^{
            
        } failure:^(NSError *error) {
            MXStrongifyAndReturnIfNil(self);
            
            [self.errorPresenter presentErrorFromViewController:self forError:error animated:YES handler:nil];
        }];
    }];
    self.emojiPickerCoordinatorBridgePresenter = nil;
}

- (void)emojiPickerCoordinatorBridgePresenter:(EmojiPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter didRemoveEmoji:(NSString *)emoji forEventId:(NSString *)eventId
{
    MXWeakify(self);
    
    [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{
        
        [self.roomDataSource removeReaction:emoji forEventId:eventId success:^{
            
        } failure:^(NSError *error) {
            MXStrongifyAndReturnIfNil(self);
            
            [self.errorPresenter presentErrorFromViewController:self forError:error animated:YES handler:nil];
        }];
    }];
    self.emojiPickerCoordinatorBridgePresenter = nil;
}

- (void)emojiPickerCoordinatorBridgePresenterDidCancel:(EmojiPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
    [coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
    self.emojiPickerCoordinatorBridgePresenter = nil;
}

#pragma mark - ReactionHistoryCoordinatorBridgePresenterDelegate

- (void)reactionHistoryCoordinatorBridgePresenterDelegateDidClose:(ReactionHistoryCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
    [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{
        self.reactionHistoryCoordinatorBridgePresenter = nil;
    }];
}

#pragma mark - CameraPresenterDelegate

- (void)cameraPresenterDidCancel:(CameraPresenter *)cameraPresenter
{
    [cameraPresenter dismissWithAnimated:YES completion:nil];
    self.cameraPresenter = nil;
}

- (void)cameraPresenter:(CameraPresenter *)cameraPresenter didSelectImage:(UIImage *)image
{
    [cameraPresenter dismissWithAnimated:YES completion:nil];
    self.cameraPresenter = nil;
    
    NSData *imageData = UIImageJPEGRepresentation(image, 1.0);
    
    // Create before sending the message in case of a discussion (direct chat)
    [self createDiscussionIfNeeded:^(BOOL readyToSend) {
        if (readyToSend && [self inputToolbarConformsToToolbarViewProtocol])
        {
            [self.inputToolbarView sendSelectedImage:imageData
                                       withMimeType:MXKUTI.jpeg.mimeType
                                 andCompressionMode:MediaCompressionHelper.defaultCompressionMode
                                isPhotoLibraryAsset:NO];
        }
        // Errors are handled at the request level. This should be improved in case of code rewriting.
    }];
}

- (void)cameraPresenter:(CameraPresenter *)cameraPresenter didSelectVideoAt:(NSURL *)url
{
    [cameraPresenter dismissWithAnimated:YES completion:nil];
    self.cameraPresenter = nil;
    
    AVURLAsset *selectedVideo = [AVURLAsset assetWithURL:url];
    [self sendVideoAsset:selectedVideo isPhotoLibraryAsset:NO];
}

#pragma mark - MediaPickerCoordinatorBridgePresenterDelegate

- (void)mediaPickerCoordinatorBridgePresenterDidCancel:(MediaPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
    [coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
    self.mediaPickerPresenter = nil;
}

- (void)mediaPickerCoordinatorBridgePresenter:(MediaPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter didSelectImageData:(NSData *)imageData withUTI:(MXKUTI *)uti
{
    [coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
    self.mediaPickerPresenter = nil;
    
    // Create before sending the message in case of a discussion (direct chat)
    [self createDiscussionIfNeeded:^(BOOL readyToSend) {
        if (readyToSend && [self inputToolbarConformsToToolbarViewProtocol])
        {
            [self.inputToolbarView sendSelectedImage:imageData
                                       withMimeType:uti.mimeType
                                 andCompressionMode:MediaCompressionHelper.defaultCompressionMode
                                isPhotoLibraryAsset:YES];
        }
        // Errors are handled at the request level. This should be improved in case of code rewriting.
    }];
}

- (void)mediaPickerCoordinatorBridgePresenter:(MediaPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter didSelectVideo:(AVAsset *)videoAsset
{
    [coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
    self.mediaPickerPresenter = nil;
    
    [self sendVideoAsset:videoAsset isPhotoLibraryAsset:YES];
}

- (void)mediaPickerCoordinatorBridgePresenter:(MediaPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter didSelectAssets:(NSArray<PHAsset *> *)assets
{
    [coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
    self.mediaPickerPresenter = nil;
    
    // Set a 1080p video conversion preset as compression mode only has an effect on the images.
    [MXSDKOptions sharedInstance].videoConversionPresetName = AVAssetExportPreset1920x1080;
    
    // Create before sending the message in case of a discussion (direct chat)
    [self createDiscussionIfNeeded:^(BOOL readyToSend) {
        if (readyToSend && [self inputToolbarConformsToToolbarViewProtocol])
        {
            [self.inputToolbarView sendSelectedAssets:assets withCompressionMode:MediaCompressionHelper.defaultCompressionMode];
        }
        // Errors are handled at the request level. This should be improved in case of code rewriting.
    }];
}

#pragma mark - RoomCreationModalCoordinatorBridgePresenter

- (void)roomCreationModalCoordinatorBridgePresenterDelegateDidComplete:(RoomCreationModalCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
    [coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
    self.roomCreationModalCoordinatorBridgePresenter = nil;
}

#pragma mark - RoomInfoCoordinatorBridgePresenterDelegate

- (void)roomInfoCoordinatorBridgePresenterDelegateDidComplete:(RoomInfoCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
    [coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
    self.roomInfoCoordinatorBridgePresenter = nil;
}

- (void)roomInfoCoordinatorBridgePresenter:(RoomInfoCoordinatorBridgePresenter *)coordinatorBridgePresenter didRequestMentionForMember:(MXRoomMember *)member
{
    [self mention:member];
}

- (void)roomInfoCoordinatorBridgePresenterDelegateDidLeaveRoom:(RoomInfoCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
    [self notifyDelegateOnLeaveRoomIfNecessary];
}

- (void)roomInfoCoordinatorBridgePresenter:(RoomInfoCoordinatorBridgePresenter *)coordinatorBridgePresenter didReplaceRoomWithReplacementId:(NSString *)roomId
{
    if (self.delegate)
    {
        [self.delegate roomViewController:self didReplaceRoomWithReplacementId:roomId];
    }
    else
    {
        ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:YES stackAboveVisibleViews:NO];
        RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId eventId:nil mxSession:self.mainSession presentationParameters:presentationParameters showSettingsInitially:YES];
        [[AppDelegate theDelegate] showRoomWithParameters:parameters];
    }
}

- (void)roomInfoCoordinatorBridgePresenter:(RoomInfoCoordinatorBridgePresenter *)coordinator
                       viewEventInTimeline:(MXEvent *)event
{
    [self.navigationController popToViewController:self animated:true];
    [self reloadRoomWihtEventId:event.eventId threadId:event.threadId forceUpdateRoomMarker:NO];
}

- (void)roomInfoCoordinatorBridgePresenterDidRequestReportRoom:(RoomInfoCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
    [self handleReportRoom];
}

-(void)reloadRoomWihtEventId:(NSString *)eventId
                    threadId:(NSString *)threadId
       forceUpdateRoomMarker:(BOOL)forceUpdateRoomMarker
{
    // Jump to the last unread event by using a temporary room data source initialized with the last unread event id.
    MXWeakify(self);
    [RoomDataSource loadRoomDataSourceWithRoomId:self.roomDataSource.roomId
                                  initialEventId:eventId
                                        threadId:threadId
                                andMatrixSession:self.mainSession
                                      onComplete:^(id roomDataSource) {
        MXStrongifyAndReturnIfNil(self);
        
        [roomDataSource finalizeInitialization];
        
        // Center the bubbles table content on the bottom of the read marker event in order to display correctly the read marker view.
        self.centerBubblesTableViewContentOnTheInitialEventBottom = YES;
        [self displayRoom:roomDataSource];
        
        // Give the data source ownership to the room view controller.
        self.hasRoomDataSourceOwnership = YES;
        
        // Force the read marker update if needed (e.g if we jumped on the last unread message using the banner).
        self.updateRoomReadMarker |= forceUpdateRoomMarker;
    }];
}

#pragma mark - RemoveJitsiWidgetViewDelegate

- (void)removeJitsiWidgetViewDidCompleteSliding:(RemoveJitsiWidgetView *)view
{
    view.delegate = nil;
    Widget *jitsiWidget = [self.customizedRoomDataSource jitsiWidget];
    
    [self startActivityIndicator];
    
    //  close the widget
    MXWeakify(self);
    
    [[WidgetManager sharedManager] closeWidget:jitsiWidget.widgetId
                                        inRoom:self.roomDataSource.room
                                       success:^{
        MXStrongifyAndReturnIfNil(self);
        [self stopActivityIndicator];
        //  we can wait for kWidgetManagerDidUpdateWidgetNotification, but we want to be faster
        self.removeJitsiWidgetContainer.hidden = YES;
        self.removeJitsiWidgetView.delegate = nil;
        
        //  end active call if exists
        if ([self isRoomHavingAJitsiCall])
        {
            [self endActiveJitsiCall];
        }
    } failure:^(NSError *error) {
        MXStrongifyAndReturnIfNil(self);
        [self showJitsiErrorAsAlert:error];
        [self stopActivityIndicator];
    }];
}

#pragma mark - VoiceMessageControllerDelegate

- (void)voiceMessageControllerDidRequestMicrophonePermission:(VoiceMessageController *)voiceMessageController
{
    NSString *message = [VectorL10n microphoneAccessNotGrantedForVoiceMessage:AppInfo.current.displayName];
    
    [MXKTools checkAccessForMediaType:AVMediaTypeAudio
                  manualChangeMessage: message
            showPopUpInViewController:self completionHandler:^(BOOL granted) {
        
    }];
}

- (BOOL)voiceMessageControllerDidRequestRecording:(VoiceMessageController *)voiceMessageController
{
    MXSession* session = self.roomDataSource.mxSession;
    // Check whether the user is not already broadcasting here or in another room
    if (session.voiceBroadcastService)
    {
        [self showAlertWithTitle:[VectorL10n voiceMessageBroadcastInProgressTitle] message:[VectorL10n voiceMessageBroadcastInProgressMessage]];

        return NO;
    }

    return YES;
}

- (void)voiceMessageController:(VoiceMessageController *)voiceMessageController
    didRequestSendForFileAtURL:(NSURL *)url
                      duration:(NSUInteger)duration
                       samples:(NSArray<NSNumber *> *)samples
                    completion:(void (^)(BOOL))completion
{
    [self.roomDataSource sendVoiceMessage:url additionalContentParams:nil mimeType:nil duration:duration samples:samples success:^(NSString *eventId) {
        MXLogDebug(@"Success with event id %@", eventId);
        completion(YES);
    } failure:^(NSError *error) {
        MXLogError(@"Failed sending voice message");
        completion(NO);
    }];
}

#pragma mark - SpaceDetailPresenterDelegate

- (void)spaceDetailPresenterDidComplete:(SpaceDetailPresenter *)presenter
{
    self.spaceDetailPresenter = nil;
}

- (void)spaceDetailPresenter:(SpaceDetailPresenter *)presenter didOpenSpaceWithId:(NSString *)spaceId
{
    self.spaceDetailPresenter = nil;
    [[LegacyAppDelegate theDelegate] openSpaceWithId:spaceId];
}

- (void)spaceDetailPresenter:(SpaceDetailPresenter *)presenter didJoinSpaceWithId:(NSString *)spaceId
{
    self.spaceDetailPresenter = nil;
    [[LegacyAppDelegate theDelegate] openSpaceWithId:spaceId];
}

#pragma mark - CompletionSuggestionCoordinatorBridgeDelegate

- (void)completionSuggestionCoordinatorBridge:(CompletionSuggestionCoordinatorBridge *)coordinator
             didRequestMentionForMember:(MXRoomMember *)member
                            textTrigger:(NSString *)textTrigger
{
    [self removeTriggerTextFromComposer:textTrigger];
    [self mention:member];
}

- (void)completionSuggestionCoordinatorBridgeDidRequestMentionForRoom:(CompletionSuggestionCoordinatorBridge *)coordinator
                                                    textTrigger:(NSString *)textTrigger
{
    [self removeTriggerTextFromComposer:textTrigger];
    [self.inputToolbarView pasteText:[CompletionSuggestionUserID.room stringByAppendingString:@" "]];
}

- (void)completionSuggestionCoordinatorBridge:(CompletionSuggestionCoordinatorBridge *)coordinator
                           didRequestCommand:(NSString *)command
                                 textTrigger:(NSString *)textTrigger
{
    [self removeTriggerTextFromComposer:textTrigger];
    [self setCommand:command];
}

- (void)removeTriggerTextFromComposer:(NSString *)textTrigger
{
    RoomInputToolbarView *toolbar = (RoomInputToolbarView *)self.inputToolbarView;
    Class roomInputToolbarViewClass = [RoomViewController mainToolbarClass];

    // RTE handles removing the text trigger by itself.
    if (roomInputToolbarViewClass == WysiwygInputToolbarView.class && RiotSettings.shared.enableWysiwygTextFormatting)
    {
        return;
    }

    if (toolbar && textTrigger.length) {
        NSMutableAttributedString *attributedTextMessage = [[NSMutableAttributedString alloc] initWithAttributedString:toolbar.attributedTextMessage];
        [[attributedTextMessage mutableString] replaceOccurrencesOfString:textTrigger
                                                               withString:@""
                                                                  options:NSBackwardsSearch | NSAnchoredSearch
                                                                    range:NSMakeRange(0, attributedTextMessage.length)];
        [toolbar setAttributedTextMessage:attributedTextMessage];
    }
}

- (void)completionSuggestionCoordinatorBridge:(CompletionSuggestionCoordinatorBridge *)coordinator didUpdateViewHeight:(CGFloat)height
{
    if (self.completionSuggestionContainerHeightConstraint.constant != height)
    {
        self.completionSuggestionContainerHeightConstraint.constant = height;

        [self.view layoutIfNeeded];
    }
}

#pragma mark - ThreadsCoordinatorBridgePresenterDelegate

- (void)threadsCoordinatorBridgePresenterDelegateDidComplete:(ThreadsCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
    self.threadsBridgePresenter = nil;
}

- (void)threadsCoordinatorBridgePresenterDelegateDidSelect:(ThreadsCoordinatorBridgePresenter *)coordinatorBridgePresenter roomId:(NSString *)roomId eventId:(NSString *)eventId
{
    MXWeakify(self);
    [self.threadsBridgePresenter dismissWithAnimated:YES completion:^{
        MXStrongifyAndReturnIfNil(self);
        
        if (eventId)
        {
            [self highlightAndDisplayEvent:eventId completion:nil];
        }
    }];
}

- (void)threadsCoordinatorBridgePresenterDidDismissInteractively:(ThreadsCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
    self.threadsBridgePresenter = nil;
}

#pragma mark - ThreadsBetaCoordinatorBridgePresenterDelegate

- (void)threadsBetaCoordinatorBridgePresenterDelegateDidTapEnable:(ThreadsBetaCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
    MXWeakify(self);
    [self.threadsBetaBridgePresenter dismissWithAnimated:YES completion:^{
        MXStrongifyAndReturnIfNil(self);
        [self cancelEventSelection];
        [self.roomDataSource reload];
        [self openThreadWithId:coordinatorBridgePresenter.threadId];
    }];
}

- (void)threadsBetaCoordinatorBridgePresenterDelegateDidTapCancel:(ThreadsBetaCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
    MXWeakify(self);
    [self.threadsBetaBridgePresenter dismissWithAnimated:YES completion:^{
        MXStrongifyAndReturnIfNil(self);
        [self cancelEventSelection];
    }];
}

#pragma mark - MXThreadingServiceDelegate

- (void)threadingServiceDidUpdateThreads:(MXThreadingService *)service
{
    [self updateThreadListBarButtonBadgeWith:service];
}

#pragma mark - RoomParticipantsInviteCoordinatorBridgePresenterDelegate

- (void)roomParticipantsInviteCoordinatorBridgePresenterDidComplete:(RoomParticipantsInviteCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
    self.participantsInvitePresenter = nil;
}

- (void)roomParticipantsInviteCoordinatorBridgePresenterDidStartLoading:(RoomParticipantsInviteCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
    [self startActivityIndicator];
}

- (void)roomParticipantsInviteCoordinatorBridgePresenterDidEndLoading:(RoomParticipantsInviteCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
    [self stopActivityIndicator];
}

#pragma mark - Pills
/// Register provider for Pills.
- (void)registerPillAttachmentViewProviderIfNeeded
{
    if (@available(iOS 15.0, *))
    {
        if (![NSTextAttachment textAttachmentViewProviderClassForFileType:PillsFormatter.pillUTType])
        {
            [NSTextAttachment registerTextAttachmentViewProviderClass:PillAttachmentViewProvider.class forFileType:PillsFormatter.pillUTType];
        }
    }
}

#pragma mark - ComposerCreateActionListBridgePresenter

- (void)composerCreateActionListBridgePresenterDelegateDidComplete:(ComposerCreateActionListBridgePresenter *)coordinatorBridgePresenter action:(enum ComposerCreateAction)action
{
    
    [coordinatorBridgePresenter dismissWithAnimated:true completion:^{
        switch (action) {
            case ComposerCreateActionPhotoLibrary:
                [self showMediaPickerAnimated:YES];
                break;
            case ComposerCreateActionStickers:
                [self roomInputToolbarViewPresentStickerPicker];
                break;
            case ComposerCreateActionAttachments:
                [self roomInputToolbarViewDidTapFileUpload];
                break;
            case ComposerCreateActionVoiceBroadcast:
                [self roomInputToolbarViewDidTapVoiceBroadcast];
                break;
            case ComposerCreateActionPolls:
                [self.delegate roomViewControllerDidRequestPollCreationFormPresentation:self];
                break;
            case ComposerCreateActionLocation:
                [self.delegate roomViewControllerDidRequestLocationSharingFormPresentation:self];
                break;
            case ComposerCreateActionCamera:
                [self showCameraControllerAnimated:YES];
                break;
        }
        self.composerCreateActionListBridgePresenter = nil;
    }];
}

- (void)composerCreateActionListBridgePresenterDelegateDidToggleTextFormatting:(ComposerCreateActionListBridgePresenter *)coordinatorBridgePresenter enabled:(BOOL)enabled
{
    [self togglePlainTextMode];
}

- (void)composerCreateActionListBridgePresenterDidDismissInteractively:(ComposerCreateActionListBridgePresenter *)coordinatorBridgePresenter
{
    self.composerCreateActionListBridgePresenter = nil;
}

@end
